Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into git-http-controller

Conflicts:
	lib/gitlab/workhorse.rb
This commit is contained in:
Jacob Vosmaer
2016-06-08 10:31:33 +02:00
394 changed files with 9523 additions and 3198 deletions
+167 -197
View File
@@ -2,7 +2,7 @@ image: "ruby:2.1"
services:
- mysql:latest
- redis:latest
- redis:alpine
cache:
key: "ruby21"
@@ -13,100 +13,187 @@ variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
# retry tests only in CI environment
RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
USE_DB: "true"
USE_BUNDLE_INSTALL: "true"
before_script:
- source ./scripts/prepare_build.sh
- ruby -v
- which ruby
- retry gem install bundler --no-ri --no-rdoc
- cp config/gitlab.yml.example config/gitlab.yml
- touch log/application.log
- touch log/test.log
- retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
- '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages:
- prepare
- test
- notifications
- post-test
spec:feature:
# Prepare and merge knapsack tests
.knapsack-state: &knapsack-state
services: []
variables:
USE_DB: "false"
USE_BUNDLE_INSTALL: "false"
cache:
key: "knapsack"
paths:
- knapsack/
artifacts:
paths:
- knapsack/
knapsack:
<<: *knapsack-state
stage: prepare
script:
- mkdir -p knapsack/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
update-knapsack:
<<: *knapsack-state
stage: post-test
script:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
# Execute all testing suites
.rspec-knapsack: &rspec-knapsack
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
- bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- knapsack rspec
artifacts:
paths:
- knapsack/
spec:api:
.spinach-knapsack: &spinach-knapsack
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
- bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun"
# retry failed tests 3 times
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
paths:
- knapsack/
spec:models:
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
rspec 3 20: *rspec-knapsack
rspec 4 20: *rspec-knapsack
rspec 5 20: *rspec-knapsack
rspec 6 20: *rspec-knapsack
rspec 7 20: *rspec-knapsack
rspec 8 20: *rspec-knapsack
rspec 9 20: *rspec-knapsack
rspec 10 20: *rspec-knapsack
rspec 11 20: *rspec-knapsack
rspec 12 20: *rspec-knapsack
rspec 13 20: *rspec-knapsack
rspec 14 20: *rspec-knapsack
rspec 15 20: *rspec-knapsack
rspec 16 20: *rspec-knapsack
rspec 17 20: *rspec-knapsack
rspec 18 20: *rspec-knapsack
rspec 19 20: *rspec-knapsack
spinach 0 10: *spinach-knapsack
spinach 1 10: *spinach-knapsack
spinach 2 10: *spinach-knapsack
spinach 3 10: *spinach-knapsack
spinach 4 10: *spinach-knapsack
spinach 5 10: *spinach-knapsack
spinach 6 10: *spinach-knapsack
spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.2
.ruby-22: &ruby-22
image: "ruby:2.2"
only:
- master
cache:
key: "ruby22"
paths:
- vendor
.rspec-knapsack-ruby22: &rspec-knapsack-ruby22
<<: *rspec-knapsack
<<: *ruby-22
.spinach-knapsack-ruby22: &spinach-knapsack-ruby22
<<: *spinach-knapsack
<<: *ruby-22
rspec 0 20 ruby22: *rspec-knapsack-ruby22
rspec 1 20 ruby22: *rspec-knapsack-ruby22
rspec 2 20 ruby22: *rspec-knapsack-ruby22
rspec 3 20 ruby22: *rspec-knapsack-ruby22
rspec 4 20 ruby22: *rspec-knapsack-ruby22
rspec 5 20 ruby22: *rspec-knapsack-ruby22
rspec 6 20 ruby22: *rspec-knapsack-ruby22
rspec 7 20 ruby22: *rspec-knapsack-ruby22
rspec 8 20 ruby22: *rspec-knapsack-ruby22
rspec 9 20 ruby22: *rspec-knapsack-ruby22
rspec 10 20 ruby22: *rspec-knapsack-ruby22
rspec 11 20 ruby22: *rspec-knapsack-ruby22
rspec 12 20 ruby22: *rspec-knapsack-ruby22
rspec 13 20 ruby22: *rspec-knapsack-ruby22
rspec 14 20 ruby22: *rspec-knapsack-ruby22
rspec 15 20 ruby22: *rspec-knapsack-ruby22
rspec 16 20 ruby22: *rspec-knapsack-ruby22
rspec 17 20 ruby22: *rspec-knapsack-ruby22
rspec 18 20 ruby22: *rspec-knapsack-ruby22
rspec 19 20 ruby22: *rspec-knapsack-ruby22
spinach 0 10 ruby22: *spinach-knapsack-ruby22
spinach 1 10 ruby22: *spinach-knapsack-ruby22
spinach 2 10 ruby22: *spinach-knapsack-ruby22
spinach 3 10 ruby22: *spinach-knapsack-ruby22
spinach 4 10 ruby22: *spinach-knapsack-ruby22
spinach 5 10 ruby22: *spinach-knapsack-ruby22
spinach 6 10 ruby22: *spinach-knapsack-ruby22
spinach 7 10 ruby22: *spinach-knapsack-ruby22
spinach 8 10 ruby22: *spinach-knapsack-ruby22
spinach 9 10 ruby22: *spinach-knapsack-ruby22
# Other generic tests
.exec: &exec
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
- bundle exec $CI_BUILD_NAME
spec:lib:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
spec:services:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
spec:other:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
spinach:project:half:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
spinach:project:rest:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
spinach:other:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
teaspoon:
stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
rubocop:
stage: test
script:
- bundle exec rubocop
scss-lint:
stage: test
script:
- bundle exec rake scss_lint
brakeman:
stage: test
script:
- bundle exec rake brakeman
flog:
stage: test
script:
- bundle exec rake flog
flay:
stage: test
script:
- bundle exec rake flay
teaspoon: *exec
rubocop: *exec
rake scss_lint: *exec
rake brakeman: *exec
rake flog: *exec
rake flay: *exec
rake db:migrate:reset: *exec
license_finder: *exec
bundler:audit:
stage: test
@@ -115,127 +202,10 @@ bundler:audit:
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
db-migrate-reset:
stage: test
script:
- RAILS_ENV=test bundle exec rake db:migrate:reset
# Ruby 2.2 jobs
spec:feature:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
cache:
key: "ruby22"
paths:
- vendor
spec:api:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
cache:
key: "ruby22"
paths:
- vendor
spec:models:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
cache:
key: "ruby22"
paths:
- vendor
spec:lib:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
cache:
key: "ruby22"
paths:
- vendor
spec:services:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
cache:
key: "ruby22"
paths:
- vendor
spec:other:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
cache:
key: "ruby22"
paths:
- vendor
spinach:project:half:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
cache:
key: "ruby22"
paths:
- vendor
spinach:project:rest:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
cache:
key: "ruby22"
paths:
- vendor
spinach:other:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
cache:
key: "ruby22"
paths:
- vendor
# Notify slack in the end
notify:slack:
stage: notifications
stage: post-test
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
when: on_failure
+2 -2
View File
@@ -194,7 +194,7 @@ Style/EmptyLines:
# Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier:
Enabled: false
Enabled: true
# Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody:
@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation
# without parentheses.
Lint/AmbiguousOperator:
Enabled: false
Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses.
View File
+50 -5
View File
@@ -1,24 +1,35 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
- Allow forking projects with restricted visibility level
- Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies
- Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
- Redesign navigation for project pages
- Fix groups API to list only user's accessible projects
- Redesign account and email confirmation emails
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
- Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix issues filter when ordering by milestone
- Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
- Pipelines can be canceled only when there are running builds
- Use downcased path to container repository as this is expected path by Docker
@@ -26,13 +37,46 @@ v 8.9.0 (unreleased)
- Measure queue duration between gitlab-workhorse and Rails
- Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
- Cache project build count in sidebar nav
- Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects
- Remove duplicated notification settings
- Put project Files and Commits tabs under Code tab
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
- An indicator is now displayed at the top of the comment field for confidential issues.
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
v 8.8.4 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
- Upgrade to jQuery 2
- Remove prev/next buttons on issues and merge requests
- Import GitHub repositories respecting the API rate limit
- Fix importer for GitHub comments on diff
- Disable Webhooks before proceeding with the GitHub import
- Added descriptions to notification settings dropdown
v 8.8.3
- Fix incorrect links on pipeline page when merge request created from fork
- Fix gitlab importer failing to import new projects due to missing credentials
- Fix import URL migration not rescuing with the correct Error
- In search results, only show notes on confidential issues that the user has access to
- Fix health check access token changing due to old application settings being used
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
- Fixed JS error when trying to remove discussion form. !4303
- Fixed issue with button color when no CI enabled. !4287
- Fixed potential issue with 2 CI status polling events happening. !3869
- Improve design of Pipeline view. !4230
- Fix gitlab importer failing to import new projects due to missing credentials. !4301
- Fix import URL migration not rescuing with the correct Error. !4321
- Fix health check access token changing due to old application settings being used. !4332
- Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363
- Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364
- Pass the "Remember me" value to the 2FA token form. !4369
- Fix incorrect links on pipeline page when merge request created from fork. !4376
- Use downcased path to container repository as this is expected path by Docker. !4420
- Fix wiki project clone address error (chujinjin). !4429
- Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392
- Fix missing number on generated ordered list element. !4437
- Prevent disclosure of notes on confidential issues in search results.
v 8.8.2
- Added remove due date button. !4209
@@ -134,6 +178,7 @@ v 8.7.6
- Fix import from GitLab.com to a private instance failure. !4181
- Fix external imports not finding the import data. !4106
- Fix notification delay when changing status of an issue
- Bump Workhorse to 0.7.5 so it can serve raw diffs
v 8.7.5
- Fix relative links in wiki pages. !4050
+5 -3
View File
@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(the PNG is 1:1).
The current designs can be found in the [`gitlab1.atype` file].
The current designs can be found in the [`gitlab8.atype` file].
### UI development kit
@@ -308,7 +308,7 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch
1. Create a feature branch, branch away from `master`.
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
@@ -405,6 +405,7 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases
@@ -530,4 +531,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
[license-finder-doc]: doc/development/licensing.md
+1 -1
View File
@@ -1 +1 @@
0.7.4
0.7.5
+15 -5
View File
@@ -38,16 +38,17 @@ gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt'
# Spam and anti-bot protection
gem 'recaptcha', require: 'recaptcha/rails'
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection
gem "browser", '~> 1.0.0'
gem "browser", '~> 2.0.3'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
@@ -83,8 +84,14 @@ gem "carrierwave", '~> 0.10.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40'
gem 'fog-local', '~> 0.3'
gem 'fog-google', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
# for aws storage
gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4'
# Authorization
@@ -104,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1'
gem 'rouge', '~> 1.11'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -137,7 +144,7 @@ gem 'redis-namespace'
gem "httparty", '~> 0.13.3'
# Colored output to console
gem "colorize", '~> 0.7.0'
gem "rainbow", '~> 2.1.0'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -299,6 +306,9 @@ group :development, :test do
gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false
gem "license_finder", require: false
gem 'knapsack'
end
group :test do
+44 -108
View File
@@ -1,7 +1,6 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (2.3.2)
RedCloth (4.2.9)
ace-rails-ap (4.0.2)
actionmailer (4.2.6)
@@ -93,7 +92,7 @@ GEM
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
browser (1.0.1)
browser (2.0.3)
builder (3.2.2)
bullet (5.0.0)
activesupport (>= 3.0.0)
@@ -183,7 +182,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
excon (0.45.4)
excon (0.49.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_girl (4.5.0)
@@ -200,8 +199,6 @@ GEM
multi_json
ffaker (2.0.0)
ffi (1.9.10)
fission (0.5.0)
CFPropertyList (~> 2.2)
flay (2.6.1)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
@@ -211,109 +208,28 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog (1.36.0)
fog-aliyun (>= 0.1.0)
fog-atmos
fog-aws (>= 0.6.0)
fog-brightbox (~> 0.4)
fog-core (~> 1.32)
fog-dynect (~> 0.0.2)
fog-ecloud (~> 0.1)
fog-google (<= 0.1.0)
fog-json
fog-local
fog-powerdns (>= 0.1.1)
fog-profitbricks
fog-radosgw (>= 0.0.2)
fog-riakcs
fog-sakuracloud (>= 0.0.4)
fog-serverlove
fog-softlayer
fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
fog-xenserver
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
fog-aliyun (0.1.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-atmos (0.1.0)
fog-core
fog-xml
fog-aws (0.8.1)
fog-aws (0.9.2)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
fog-core (1.35.0)
fog-core (1.40.0)
builder
excon (~> 0.45)
excon (~> 0.49)
formatador (~> 0.2)
fog-dynect (0.0.2)
fog-core
fog-json
fog-xml
fog-ecloud (0.3.0)
fog-core
fog-xml
fog-google (0.1.0)
fog-google (0.3.2)
fog-core
fog-json
fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-local (0.2.1)
fog-local (0.3.0)
fog-core (~> 1.27)
fog-powerdns (0.1.1)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
fog-profitbricks (0.0.5)
fog-core
fog-xml
nokogiri
fog-radosgw (0.0.5)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-riakcs (0.1.0)
fog-core
fog-json
fog-xml
fog-sakuracloud (1.7.5)
fog-core
fog-json
fog-serverlove (0.1.2)
fog-core
fog-json
fog-softlayer (1.0.3)
fog-core
fog-json
fog-storm_on_demand (0.1.1)
fog-core
fog-json
fog-terremark (0.1.0)
fog-core
fog-xml
fog-vmfusion (0.1.0)
fission
fog-core
fog-voxel (0.1.0)
fog-core
fog-xml
fog-xenserver (0.2.2)
fog-core
fog-xml
fog-openstack (0.1.6)
fog-core (>= 1.39)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
@@ -422,11 +338,10 @@ GEM
httpclient (2.7.0.1)
i18n (0.7.0)
ice_nine (0.11.1)
inflecto (0.0.2)
influxdb (0.2.3)
cause
json
ipaddress (0.8.2)
ipaddress (0.8.3)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3)
@@ -443,6 +358,9 @@ GEM
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
kgio (2.10.0)
knapsack (1.11.0)
rake
timecop (>= 0.1.0)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -451,6 +369,12 @@ GEM
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
license_finder (2.1.0)
bundler
httparty
rubyzip
thor
xml-simple
licensee (8.0.0)
rugged (>= 0.24b)
listen (3.0.5)
@@ -466,7 +390,7 @@ GEM
method_source (0.8.2)
mime-types (2.99.1)
mimemagic (0.3.0)
mini_portile2 (2.0.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.11.2)
@@ -477,8 +401,9 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
@@ -550,6 +475,7 @@ GEM
parser (2.3.1.0)
ast (~> 2.2)
pg (0.18.4)
pkg-config (1.1.7)
poltergeist (1.9.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -625,7 +551,7 @@ GEM
debugger-ruby_core_source (~> 1.3)
rdoc (3.12.2)
json (~> 1.4)
recaptcha (1.0.2)
recaptcha (3.0.0)
json
redcarpet (3.3.3)
redis (3.3.0)
@@ -654,7 +580,7 @@ GEM
railties (>= 4.2.0, < 5.1)
rinku (1.7.3)
rotp (2.1.2)
rouge (1.10.1)
rouge (1.11.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -703,6 +629,7 @@ GEM
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
rubyzip (1.2.0)
rufus-scheduler (3.1.10)
rugged (0.24.0)
safe_yaml (1.0.4)
@@ -813,6 +740,7 @@ GEM
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.2)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
eventmachine (~> 1.0)
@@ -832,6 +760,7 @@ GEM
simple_oauth (~> 0.1.4)
tzinfo (1.2.2)
thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
@@ -900,7 +829,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0)
browser (~> 1.0.0)
browser (~> 2.0.3)
bullet
bundler-audit
byebug
@@ -909,7 +838,6 @@ DEPENDENCIES
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
@@ -927,7 +855,11 @@ DEPENDENCIES
ffaker (~> 2.0.0)
flay
flog
fog (~> 1.36.0)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-google (~> 0.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
@@ -956,7 +888,9 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0)
jwt
kaminari (~> 0.17.0)
knapsack
letter_opener_web (~> 1.3.0)
license_finder
licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.7)
@@ -996,10 +930,11 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1)
rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
recaptcha
recaptcha (~> 3.0)
redcarpet (~> 3.3.3)
redis (~> 3.2)
redis-namespace
@@ -1007,7 +942,7 @@ DEPENDENCIES
request_store (~> 1.3.0)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 1.10.1)
rouge (~> 1.11)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.4.0)
rspec-retry
@@ -1045,6 +980,7 @@ DEPENDENCIES
thin (~> 1.6.1)
tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
@@ -1057,4 +993,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.12.4
1.12.5
+2
View File
@@ -8,3 +8,5 @@ relative_url_conf = File.expand_path('../config/initializers/relative_url', __FI
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks
Knapsack.load_tasks if defined?(Knapsack)
@@ -0,0 +1,84 @@
class @LabelManager
errorMessage: 'Unable to update label prioritization at this time'
constructor: (opts = {}) ->
# Defaults
{
@togglePriorityButton = $('.js-toggle-priority')
@prioritizedLabels = $('.js-prioritized-labels')
@otherLabels = $('.js-other-labels')
} = opts
@prioritizedLabels.sortable(
items: 'li'
placeholder: 'list-placeholder'
axis: 'y'
update: @onPrioritySortUpdate.bind(@)
)
@bindEvents()
bindEvents: ->
@togglePriorityButton.on 'click', @, @onTogglePriorityClick
onTogglePriorityClick: (e) ->
e.preventDefault()
_this = e.data
$btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
_this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) ->
_this = @
url = $label.find('.js-toggle-priority').data 'url'
$target = @prioritizedLabels
$from = @otherLabels
# Optimistic update
if action is 'remove'
$target = @otherLabels
$from = @prioritizedLabels
if $from.find('li').length is 1
$from.find('.empty-message').show()
if not $target.find('li').length
$target.find('.empty-message').hide()
$label.detach().appendTo($target)
# Return if we are not persisting state
return unless persistState
if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE'
else
xhr = @savePrioritySort($label, action)
xhr.fail @rollbackLabelPosition.bind(@, $label, action)
onPrioritySortUpdate: ->
xhr = @savePrioritySort()
xhr.fail ->
new Flash(@errorMessage, 'alert')
savePrioritySort: () ->
$.post
url: @prioritizedLabels.data('url')
data:
label_ids: @getSortedLabelsIds()
rollbackLabelPosition: ($label, originalAction)->
action = if originalAction is 'remove' then 'add' else 'remove'
@toggleLabelPriority($label, action, false)
new Flash(@errorMessage, 'alert')
getSortedLabelsIds: ->
sortedIds = []
@prioritizedLabels.find('li').each ->
sortedIds.push $(@).data 'id'
sortedIds
+3 -1
View File
@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require jquery
#= require jquery2
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
#= require jquery-ui/draggable
@@ -56,9 +56,11 @@
#= require_directory ./commit
#= require_directory ./extensions
#= require_directory ./lib
#= require_directory ./u2f
#= require_directory .
#= require fuzzaldrin-plus
#= require cropper
#= require u2f
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
+284 -131
View File
@@ -1,201 +1,352 @@
class @AwardsHandler
constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
$('.js-add-award').on 'click', (event) =>
event.stopPropagation()
event.preventDefault()
@showEmojiMenu()
constructor: ->
$('html').on 'click', (event) ->
if !$(event.target).closest('.emoji-menu').length
@aliases = gl.emojiAliases()
$(document)
.off 'click', '.js-add-award'
.on 'click', '.js-add-award', (e) =>
e.stopPropagation()
e.preventDefault()
@showEmojiMenu $(e.currentTarget)
$('html').on 'click', (e) ->
$target = $ e.target
unless $target.closest('.emoji-menu-content').length
$('.js-awards-block.current').removeClass 'current'
unless $target.closest('.emoji-menu').length
if $('.emoji-menu').is(':visible')
$('.js-add-award.is-active').removeClass 'is-active'
$('.emoji-menu').removeClass 'is-visible'
$('.awards')
.off 'click'
.on 'click', '.js-emoji-btn', @handleClick
$(document)
.off 'click', '.js-emoji-btn'
.on 'click', '.js-emoji-btn', (e) =>
e.preventDefault()
@renderFrequentlyUsedBlock()
$target = $ e.currentTarget
emoji = $target.find('.icon').data 'emoji'
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find('.icon')
.data 'emoji'
$target.closest('.js-awards-block').addClass 'current'
@addAward @getVotesBlock(), @getAwardUrl(), emoji
if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
awardsHandler.addAward 'thumbsdown'
else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
awardsHandler.addAward 'thumbsup'
showEmojiMenu: ($addBtn) ->
awardsHandler.addAward emoji
$menu = $ '.emoji-menu'
$(this).trigger 'blur'
if $addBtn.hasClass 'js-note-emoji'
$addBtn.parents('.note').find('.js-awards-block').addClass 'current'
else
$addBtn.closest('.js-awards-block').addClass 'current'
didUserClickEmoji: (that, emoji) ->
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
$(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
if $menu.length
$holder = $addBtn.closest('.js-award-holder')
showEmojiMenu: ->
if $('.emoji-menu').length
if $('.emoji-menu').is '.is-visible'
$('.emoji-menu').removeClass 'is-visible'
if $menu.is '.is-visible'
$addBtn.removeClass 'is-active'
$menu.removeClass 'is-visible'
$('#emoji_search').blur()
else
$('.emoji-menu').addClass 'is-visible'
$addBtn.addClass 'is-active'
@positionMenu($menu, $addBtn)
$menu.addClass 'is-visible'
$('#emoji_search').focus()
else
$('.js-add-award').addClass 'is-loading'
$.get @getEmojisUrl, (response) =>
$('.js-add-award').removeClass 'is-loading'
$('.js-award-holder').append response
$addBtn.addClass 'is-loading is-active'
url = @getAwardMenuUrl()
@createEmojiMenu url, =>
$addBtn.removeClass 'is-loading'
$menu = $('.emoji-menu')
@positionMenu($menu, $addBtn)
@renderFrequentlyUsedBlock()
setTimeout =>
$('.emoji-menu').addClass 'is-visible'
$menu.addClass 'is-visible'
$('#emoji_search').focus()
@setupSearch()
, 200
addAward: (emoji) ->
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
createEmojiMenu: (awardMenuUrl, callback) ->
$.get awardMenuUrl, (response) ->
$('body').append response
callback()
positionMenu: ($menu, $addBtn) ->
position = $addBtn.data('position')
# The menu could potentially be off-screen or in a hidden overflow element
# So we position the element absolute in the body
css =
top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
if position? and position is 'right'
css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
$menu.addClass 'is-aligned-right'
else
css.left = "#{$addBtn.offset().left}px"
$menu.removeClass 'is-aligned-right'
$menu.css(css)
addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes, callback) ->
emoji = @normilizeEmojiName emoji
@postEmoji awardUrl, emoji, =>
@addAwardToEmojiBar votesBlock, emoji, checkMutuality
callback?()
$('.emoji-menu').removeClass 'is-visible'
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
if @exist(emoji)
if @isActive(emoji)
@decrementCounter(emoji)
addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = yes) ->
@checkMutuality votesBlock, emoji if checkForMutuality
@addEmojiToFrequentlyUsedList emoji
emoji = @normilizeEmojiName emoji
$emojiButton = @findEmojiIcon(votesBlock, emoji).parent()
if $emojiButton.length > 0
if @isActive $emojiButton
@decrementCounter $emojiButton, emoji
else
counter = @findEmojiIcon(emoji).siblings('.js-counter')
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass('active')
@addMeToAuthorList(emoji)
counter = $emojiButton.find '.js-counter'
counter.text parseInt(counter.text()) + 1
$emojiButton.addClass 'active'
@addMeToUserList votesBlock, emoji
@animateEmoji $emojiButton
else
@createEmoji(emoji)
votesBlock.removeClass 'hidden'
@createEmoji votesBlock, emoji
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass('active')
getVotesBlock: ->
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings('.js-counter')
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
emojiIcon.removeClass('active')
@removeMeFromAuthorList(emoji)
else if emoji == 'thumbsup' || emoji == 'thumbsdown'
emojiIcon.tooltip('destroy')
counter.text(0)
emojiIcon.removeClass('active')
@removeMeFromAuthorList(emoji)
currentBlock = $ '.js-awards-block.current'
return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0
getAwardUrl: -> return @getVotesBlock().data 'award-url'
checkMutuality: (votesBlock, emoji) ->
awardUrl = @getAwardUrl()
if emoji in [ 'thumbsup', 'thumbsdown' ]
mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
$emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent()
isAlreadyVoted = $emojiButton.hasClass 'active'
if isAlreadyVoted
@showEmojiLoader $emojiButton
@addAward votesBlock, awardUrl, mutualVote, no, ->
$emojiButton.removeClass 'is-loading'
showEmojiLoader: ($emojiButton) ->
$loader = $emojiButton.find '.fa-spinner'
unless $loader.length
$emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'
$emojiButton.addClass 'is-loading'
isActive: ($emojiButton) -> $emojiButton.hasClass 'active'
decrementCounter: ($emojiButton, emoji) ->
counter = $ '.js-counter', $emojiButton
counterNumber = parseInt counter.text(), 10
if counterNumber > 1
counter.text counterNumber - 1
@removeMeFromUserList $emojiButton, emoji
else if emoji is 'thumbsup' or emoji is 'thumbsdown'
$emojiButton.tooltip 'destroy'
counter.text '0'
@removeMeFromUserList $emojiButton, emoji
@removeEmoji $emojiButton if $emojiButton.parents('.note').length
else
emojiIcon.tooltip('destroy')
emojiIcon.remove()
@removeEmoji $emojiButton
$emojiButton.removeClass 'active'
removeEmoji: ($emojiButton) ->
$emojiButton.tooltip('destroy')
$emojiButton.remove()
$votesBlock = @getVotesBlock()
if $votesBlock.find('.js-emoji-btn').length is 0
$votesBlock.addClass 'hidden'
getAwardTooltip: ($awardBlock) ->
return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or ''
removeMeFromUserList: ($emojiButton, emoji) ->
awardBlock = $emojiButton
originalTitle = @getAwardTooltip awardBlock
authors = originalTitle.split ', '
authors.splice authors.indexOf('me'), 1
newAuthors = authors.join ', '
removeMeFromAuthorList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent()
authors = awardBlock
.attr('data-original-title')
.split(', ')
authors.splice(authors.indexOf('me'),1)
awardBlock
.closest('.js-emoji-btn')
.attr('data-original-title', authors.join(', '))
@resetTooltip(awardBlock)
.closest '.js-emoji-btn'
.removeData 'original-title'
.attr 'data-original-title', newAuthors
@resetTooltip awardBlock
addMeToUserList: (votesBlock, emoji) ->
awardBlock = @findEmojiIcon(votesBlock, emoji).parent()
origTitle = @getAwardTooltip awardBlock
users = []
addMeToAuthorList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent()
origTitle = awardBlock.attr('data-original-title').trim()
authors = []
if origTitle
authors = origTitle.split(', ')
authors.push('me')
awardBlock.attr('data-original-title', authors.join(', '))
@resetTooltip(awardBlock)
users = origTitle.trim().split ', '
users.push 'me'
awardBlock.attr 'title', users.join ', '
@resetTooltip awardBlock
resetTooltip: (award) ->
award.tooltip('destroy')
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
award.tooltip 'destroy'
# 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
cb = -> award.tooltip()
setTimeout cb, 200
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
createEmoji_: (votesBlock, emoji) ->
nodes = []
nodes.push(
"<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>",
"</button>"
)
emojiCssClass = @resolveNameToCssClass emoji
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
<span class='award-control-text js-counter'>1</span>
</button>"
$(nodes.join("\n"))
.insertBefore('.js-award-holder')
.find('.emoji-icon')
.data('emoji', emoji)
$emojiButton = $ buttonHtml
$emojiButton
.insertBefore votesBlock.find '.js-award-holder'
.find '.emoji-icon'
.data 'emoji', emoji
@animateEmoji $emojiButton
$('.award-control').tooltip()
votesBlock.removeClass 'current'
animateEmoji: ($emoji) ->
className = 'pulse animated'
$emoji.addClass className
setTimeout (-> $emoji.removeClass className), 321
createEmoji: (votesBlock, emoji) ->
if $('.emoji-menu').length
return @createEmoji_ votesBlock, emoji
@createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji
getAwardMenuUrl: -> return gl.awardMenuUrl
resolveNameToCssClass: (emoji) ->
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']"
if emojiIcon.length > 0
unicodeName = emojiIcon.data('unicode-name')
unicodeName = emojiIcon.data 'unicode-name'
else
# Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name'
"emoji-#{unicodeName}"
return "emoji-#{unicodeName}"
postEmoji: (emoji, callback) ->
$.post @postEmojiUrl, { note: {
note: ":#{emoji}:"
noteable_type: @noteableType
noteable_id: @noteableId
}},(data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
postEmoji: (awardUrl, emoji, callback) ->
$.post awardUrl, { name: emoji }, (data) ->
callback() if data.ok
findEmojiIcon: (votesBlock, emoji) ->
return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']"
scrollToAwards: ->
$('body, html').animate({
scrollTop: $('.awards').offset().top - 80
}, 200)
options = scrollTop: $('.awards').offset().top - 110
$('body, html').animate options, 200
normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji
addEmojiToFrequentlyUsedList: (emoji) ->
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
frequentlyUsedEmojis.push(emoji)
$.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 })
frequentlyUsedEmojis.push emoji
$.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }
getFrequentlyUsedEmojis: ->
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',')
_.compact(_.uniq(frequentlyUsedEmojis))
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',')
return _.compact _.uniq frequentlyUsedEmojis
renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis')
if $.cookie 'frequently_used_emojis'
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
ul = $('<ul>')
ul = $("<ul class='clearfix emoji-menu-list'>")
for emoji in frequentlyUsedEmojis
do (emoji) ->
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
setupSearch: ->
$('input.emoji-search').keyup (ev) =>
$('input.emoji-search').on 'keyup', (ev) =>
term = $(ev.target).val()
# Clean previous search results
@@ -204,12 +355,14 @@ class @AwardsHandler
if term
# Generate a search result block
h5 = $('<h5>').text('Search results').addClass('emoji-search')
foundEmojis = @searchEmojis(term).show()
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis)
found_emojis = @searchEmojis(term).show()
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
$('.emoji-menu-content ul, .emoji-menu-content h5').hide()
$('.emoji-menu-content').append(h5).append(ul)
else
$('.emoji-menu-content').children().show()
searchEmojis: (term)->
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
searchEmojis: (term) ->
$(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
@@ -17,11 +17,13 @@ class Dispatcher
switch page
when 'projects:issues:index'
Issuable.init()
new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
gl.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'dashboard:todos:index'
@@ -52,6 +54,7 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
gl.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
@@ -97,6 +100,8 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
when 'projects:labels:index'
new LabelManager() if $('.prioritized-labels').length
when 'projects:network:show'
# Ensure we don't create a particular shortcut handler here. This is
# already created, where the network graph is created.
@@ -21,7 +21,7 @@ class @DueDateSelect
$dropdown.glDropdown(
hidden: ->
$selectbox.hide()
$value.removeAttr('style')
$value.css('display', '')
)
addDueDate = (isDropdown) ->
@@ -42,12 +42,13 @@ class @DueDateSelect
type: 'PUT'
url: issueUpdateURL
data: data
dataType: 'json'
beforeSend: ->
$loading.fadeIn()
if isDropdown
$dropdown.trigger('loading.gl.dropdown')
$selectbox.hide()
$value.removeAttr('style')
$value.css('display', '')
$valueContent.html(mediumDate)
$sidebarValue.html(mediumDate)
+1 -1
View File
@@ -1,5 +1,5 @@
class @Flash
constructor: (message, type)->
constructor: (message, type = 'alert')->
@flash = $(".flash-container")
@flash.html("")
+65 -30
View File
@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
@indeterminateIds = []
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13
return false
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur keyCode
search_text = @input.val()
# Only filter asynchronously only if option remote is set
if @options.remote
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur keyCode
if blur_field and @filterInputBlur
@input.blur()
if blur_field and @filterInputBlur
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.query @input.val(), (data) =>
@options.callback(data)
else
@filter search_text
, 250
, 250
else
@filter @input.val()
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data
@parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
}
# Init filterable
@@ -298,6 +298,13 @@ class GitLabDropdown
opened: =>
@addArrowKeyEvent()
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
@@ -309,12 +316,18 @@ class GitLabDropdown
hidden: (e) =>
@removeArrayKeyEvent()
$input = @dropdown.find(".dropdown-input-field")
if @options.filterable
@dropdown
.find(".dropdown-input-field")
$input
.blur()
.val("")
.trigger("keyup")
# Triggering 'keyup' will re-render the dropdown which is not always required
# specially if we want to keep the state of the dropdown needed for bulk-assignment
if not @options.persistWhenHide
$input.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
@@ -358,7 +371,7 @@ class GitLabDropdown
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
html = @options.renderRow.call(@options, data, @)
else
if not selected
value = if @options.id then @options.id(data) else data.id
@@ -443,6 +456,17 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
selectedObject
else if el.hasClass(INDETERMINATE_CLASS)
el.addClass ACTIVE_CLASS
el.removeClass INDETERMINATE_CLASS
if not value?
field.remove()
if not field.length and fieldName
@addInput(fieldName, value)
return selectedObject
else
if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@@ -459,31 +483,42 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value?
if !field.length and fieldName
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
@addInput(fieldName, value)
else
field.val value
return selectedObject
selectRowAtIndex: (index) ->
selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
addInput: (fieldName, value)->
# Create hidden input for form
$input = $('<input>').attr('type', 'hidden')
.attr('name', fieldName)
.val(value)
if @options.inputId?
$input.attr('id', @options.inputId)
@dropdown.before $input
selectRowAtIndex: (e, index) ->
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
$(selector, @dropdown).trigger "click"
$el = $(selector, @dropdown)
if $el.length
e.preventDefault()
e.stopImmediatePropagation()
$(selector, @dropdown)[0].click()
addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field")
selector = '.dropdown-content li:not(.divider)'
selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
@@ -511,8 +546,8 @@ class GitLabDropdown
return false
if currentKeyCode is 13
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
if currentKeyCode is 13 and currentIndex isnt -1
@selectRowAtIndex e, currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
@@ -0,0 +1,109 @@
class @IssuableBulkActions
constructor: (opts = {}) ->
# Set defaults
{
@container = $('.content')
@form = @getElement('.bulk-update')
@issues = @getElement('.issues-list .issue')
} = opts
@bindEvents()
getElement: (selector) ->
@container.find selector
bindEvents: ->
@form.off('submit').on('submit', @onFormSubmit.bind(@))
onFormSubmit: (e) ->
e.preventDefault()
@submit()
submit: ->
_this = @
xhr = $.ajax
url: @form.attr 'action'
method: @form.attr 'method'
dataType: 'JSON',
data: @getFormDataAsObject()
xhr.done (response, status, xhr) ->
location.reload()
xhr.fail ->
new Flash("Issue update failed")
xhr.always @onFormSubmitAlways.bind(@)
onFormSubmitAlways: ->
@form.find('[type="submit"]').enable()
getSelectedIssues: ->
@issues.has('.selected_issue:checked')
getLabelsFromSelection: ->
labels = []
@getSelectedIssues().map ->
_labels = $(@).data('labels')
if _labels
_labels.map (labelId) ->
labels.push(labelId) if labels.indexOf(labelId) is -1
labels
###*
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
###
getUnmarkedIndeterminedLabels: ->
result = []
labelsToKeep = []
for el in @getElement('.labels-filter .is-indeterminate')
labelsToKeep.push $(el).data('labelId')
for id in @getLabelsFromSelection()
# Only the ones that we are not going to keep
result.push(id) if labelsToKeep.indexOf(id) is -1
result
###*
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
###
getFormDataAsObject: ->
formData =
update:
state_event : @form.find('input[name="update[state_event]"]').val()
assignee_id : @form.find('input[name="update[assignee_id]"]').val()
milestone_id : @form.find('input[name="update[milestone_id]"]').val()
issues_ids : @form.find('input[name="update[issues_ids]"]').val()
add_label_ids : []
remove_label_ids : []
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
formData
getLabelsToApply: ->
labelIds = []
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
+60 -10
View File
@@ -1,5 +1,7 @@
class @LabelsSelect
constructor: ->
_this = @
$('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown)
projectId = $dropdown.data('project-id')
@@ -196,10 +198,18 @@ class @LabelsSelect
callback data
renderRow: (label) ->
removesAll = label.id is 0 or not label.id?
renderRow: (label, instance) ->
$li = $('<li>')
$a = $('<a href="#">')
selectedClass = []
removesAll = label.id is 0 or not label.id?
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -230,13 +240,17 @@ class @LabelsSelect
else
colorEl = ''
"<li>
<a href='#' class='#{selectedClass.join(' ')}'>
#{colorEl}
#{_.escape(label.title)}
</a>
</li>"
filterable: true
# We need to identify which items are actually labels
if label.id
selectedClass.push('label-item')
$a.attr('data-label-id', label.id)
$a.addClass(selectedClass.join(' '))
.html("#{colorEl} #{_.escape(label.title)}")
# Return generated html
$li.html($a).prop('outerHTML')
persistWhenHide: $dropdown.data('persistWhenHide')
search:
fields: ['title']
selectable: true
@@ -280,10 +294,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
saveLabelData()
if not $dropdown.hasClass 'js-filter-bulk-update'
saveLabelData()
if $dropdown.hasClass('js-filter-bulk-update')
# If we are persisting state we need the classes
if not @options.persistWhenHide
$dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
if $dropdown.hasClass('js-filter-bulk-update')
return
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
@@ -298,4 +321,31 @@ class @LabelsSelect
return
else
saveLabelData()
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
)
@bindEvents()
bindEvents: ->
$('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
onSelectCheckboxIssue: ->
return if $('.selected_issue:checked').length
# Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
# Also restore button text
$('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
getIndeterminateIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
@@ -0,0 +1,2 @@
gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
@@ -83,7 +83,7 @@ class @MilestoneSelect
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
$value.css('display', '')
clicked: (selected) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
@@ -118,7 +118,7 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
$value.removeAttr('style')
$value.css('display', '')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
+4 -3
View File
@@ -162,13 +162,14 @@ class @Notes
renderNote: (note) ->
unless note.valid
if note.award
flash = new Flash('You have already used this award emoji!', 'alert')
flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content')
return
if note.award
awardsHandler.addAwardToEmojiBar(note.note)
awardsHandler.scrollToAwards()
votesBlock = $('.js-awards-block').eq 0
gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list
# or skip if rendered
@@ -20,8 +20,7 @@ class @SearchAutocomplete
@dropdown = @wrap.find('.dropdown')
@dropdownContent = @dropdown.find('.dropdown-content')
@locationBadgeEl = @getElement('.search-location-badge')
@locationText = @getElement('.location-text')
@locationBadgeEl = @getElement('.location-badge')
@scopeInputEl = @getElement('#scope')
@searchInput = @getElement('.search-input')
@projectInputEl = @getElement('#search_project_id')
@@ -133,7 +132,7 @@ class @SearchAutocomplete
scope: @scopeInputEl.val()
# Location badge
_location: @locationText.text()
_location: @locationBadgeEl.text()
}
bindEvents: ->
@@ -143,23 +142,28 @@ class @SearchAutocomplete
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
@clearInput.on 'click', @onClearInputClick
@locationBadgeEl.on 'click', =>
@searchInput.focus()
onDocumentClick: (e) =>
# If clicking outside the search box
# And search input is not focused
# And we are not clicking inside a suggestion
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length
@onSearchInputBlur()
enableAutocomplete: ->
# No need to enable anything if user is not logged in
return if !gon.current_user_id
_this = @
@loadingSuggestions = false
unless @dropdown.hasClass('open')
_this = @
@loadingSuggestions = false
@dropdown.addClass('open')
@searchInput.removeClass('disabled')
@dropdown
.addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: =>
# Saves last length of the entered text
@@ -190,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete()
else
# We should display the menu only when input is not empty
@enableAutocomplete()
@enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value
@@ -221,10 +225,8 @@ class @SearchAutocomplete
category = if item.category? then "#{item.category}: " else ''
value = if item.value? then item.value else ''
html = "<span class='location-badge'>
<i class='location-text'>#{category}#{value}</i>
</span>"
@locationBadgeEl.html(html)
badgeText = "#{category}#{value}"
@locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
restoreOriginalState: ->
@@ -233,9 +235,8 @@ class @SearchAutocomplete
for input in inputs
@getElement("##{input}").val(@originalState[input])
if @originalState._location is ''
@locationBadgeEl.empty()
@locationBadgeEl.hide()
else
@addLocationBadge(
value: @originalState._location
@@ -244,7 +245,7 @@ class @SearchAutocomplete
@dropdown.removeClass 'open'
badgePresent: ->
@locationBadgeEl.children().length
@locationBadgeEl.length
resetSearchState: ->
inputs = Object.keys @originalState
@@ -257,7 +258,7 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
removeLocationBadge: ->
@locationBadgeEl.empty()
@locationBadgeEl.hide()
# Reset state
@resetSearchState()
@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@replyWithSelectedText()
return false
)
Mousetrap.bind('j', =>
@prevIssue()
return false
)
Mousetrap.bind('k', =>
@nextIssue()
return false
)
Mousetrap.bind('e', =>
@editIssue()
return false
@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
else
@enabledHelp.push('.hidden-shortcut.issues')
prevIssue: ->
$prevBtn = $('.prev-btn')
if not $prevBtn.hasClass('disabled')
Turbolinks.visit($prevBtn.attr('href'))
nextIssue: ->
$nextBtn = $('.next-btn')
if not $nextBtn.hasClass('disabled')
Turbolinks.visit($nextBtn.attr('href'))
replyWithSelectedText: ->
if window.getSelection
selected = window.getSelection().toString()
@@ -0,0 +1,63 @@
# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> authenticated -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FAuthenticate
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@challenges = u2fParams.challenges
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
authenticate: () =>
u2f.sign(@appId, @challenges, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderAuthenticated(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
"inProgress": '#js-authenticate-u2f-in-progress',
"error": '#js-authenticate-u2f-error',
"authenticated": '#js-authenticate-u2f-authenticated'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-login-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@authenticate()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderAuthenticated: (deviceResponse) =>
@renderTemplate('authenticated')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
@@ -0,0 +1,13 @@
class @U2FError
constructor: (@errorCode) ->
@httpsDisabled = (window.location.protocol isnt 'https:')
console.error("U2F Error Code: #{@errorCode}")
message: () =>
switch
when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
"U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
"This device has already been registered with us."
else
"There was a problem communicating with your device."
@@ -0,0 +1,63 @@
# Register U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> registered -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FRegister
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@registerRequests = u2fParams.register_requests
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
register: () =>
u2f.register(@appId, @registerRequests, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderRegistered(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
"inProgress": '#js-register-u2f-in-progress',
"error": '#js-register-u2f-error',
"registered": '#js-register-u2f-registered'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-setup-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@register()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderRegistered: (deviceResponse) =>
@renderTemplate('registered')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
@@ -0,0 +1,15 @@
# Helper class for U2F (universal 2nd factor) device registration and authentication.
class @U2FUtil
@isU2FSupported: ->
if @testMode
true
else
gon.u2f.browser_supports_u2f
@enableTestMode: ->
@testMode = true
<% if Rails.env.test? %>
U2FUtil.enableTestMode();
<% end %>
@@ -149,7 +149,7 @@ class @UsersSelect
hidden: (e) ->
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
$value.css('display', '')
clicked: (user) ->
page = $('body').data 'page'
@@ -61,6 +61,11 @@
margin-bottom: -$gl-padding;
}
&.content-component-block {
padding: 11px 0;
background-color: $white-light;
}
.title {
color: $gl-text-color;
}
@@ -122,10 +122,8 @@
a {
display: block;
position: relative;
padding-left: 10px;
padding-right: 10px;
padding: 5px 10px;
color: $dropdown-link-color;
line-height: 34px;
text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
@@ -162,6 +160,16 @@
}
}
.dropdown-menu-large {
width: 340px;
}
.dropdown-menu-no-wrap {
a {
white-space: normal;
}
}
.dropdown-menu-full-width {
width: 100%;
}
@@ -232,13 +240,11 @@
a {
padding-left: 25px;
&.is-active {
&.is-indeterminate, &.is-active {
&::before {
content: "\f00c";
position: absolute;
left: 5px;
top: 50%;
margin-top: -7px;
top: 8px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
@@ -246,6 +252,14 @@
-moz-osx-font-smoothing: grayscale;
}
}
&.is-indeterminate::before {
content: "\f068";
}
&.is-active::before {
content: "\f00c";
}
}
}
@@ -525,3 +539,14 @@
background-color: $calendar-unselectable-bg;
}
}
.dropdown-menu-inner-title {
display: block;
color: $gl-title-color;
font-weight: 600;
}
.dropdown-menu-inner-content {
display: block;
color: $gl-placeholder-color;
}
@@ -89,8 +89,11 @@
}
}
$theme-blue: #2980b9;
$theme-charcoal: #3d454d;
$theme-charcoal-dark: #383f45;
$theme-charcoal-text: #b9bbbe;
$theme-blue: #2980b9;
$theme-graphite: #666;
$theme-gray: #373737;
$theme-green: #019875;
@@ -102,7 +105,7 @@ body {
}
&.ui_charcoal {
@include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41);
@include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
}
&.ui_graphite {
+13 -22
View File
@@ -79,6 +79,10 @@ header {
&.header-collapsed {
padding: 0 16px;
.side-nav-toggle {
display: block;
}
}
.side-nav-toggle {
@@ -86,6 +90,7 @@ header {
position: absolute;
left: -10px;
margin: 6px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $background-color;
@@ -97,10 +102,6 @@ header {
&:focus {
outline: none;
}
@media (max-width: $screen-xs-min) {
display: block;
}
}
}
@@ -171,31 +172,21 @@ header {
}
}
@mixin collapsed-header {
margin-left: $sidebar_collapsed_width;
}
.header-collapsed {
margin-left: $sidebar_collapsed_width;
margin-left: 0;
@media (min-width: $screen-md-min) {
@include collapsed-header;
}
@media (max-width: $screen-xs-min) {
margin-left: 0;
.header-content {
padding-left: 30px;
transition-duration: .3s;
}
}
.header-expanded {
margin-left: $sidebar_collapsed_width;
margin-left: 0;
@media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
@media (max-width: $screen-xs-min) {
margin-left: 0;
.header-content {
padding-left: $sidebar_width;
transition-duration: .3s;
}
}
@@ -141,6 +141,18 @@ ul.content-list {
padding: 10px 14px;
}
}
// When dragging a list item
&.ui-sortable-helper {
border-bottom: none;
}
&.list-placeholder {
background-color: $gray-light;
border: dotted 1px $gray-dark;
margin: 1px 0;
min-height: 30px;
}
}
}
@@ -2,18 +2,10 @@
* Generic mixins
*/
@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius;
}
@@ -66,10 +66,6 @@
display: none;
}
%ul.notes .note-role, .note-actions {
display: none;
}
.nav-links, .nav-links {
li a {
font-size: 14px;
+49 -3
View File
@@ -41,8 +41,7 @@
a {
display: inline-block;
padding: 14px;
padding-top: $gl-padding;
padding: $gl-btn-padding;
padding-bottom: 11px;
margin-bottom: -1px;
font-size: 15px;
@@ -67,6 +66,27 @@
color: #78a;
}
}
&.sub-nav {
background-color: $background-color;
.container-fluid {
background-color: $background-color;
}
li {
a {
margin: 0;
padding: 11px 10px 9px;
}
&.active a {
border-bottom: none;
color: $link-underline-blue;
}
}
}
}
.top-area {
@@ -81,6 +101,10 @@
width: 50%;
line-height: 28px;
&.wiki-page {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
@@ -104,6 +128,10 @@
margin-bottom: 0;
border-bottom: none;
li a {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-max) {
width: 100%;
@@ -309,8 +337,8 @@
}
.nav-control {
.fade-right {
.fade-right {
@media (min-width: $screen-xs-max) {
right: 67px;
}
@@ -321,6 +349,24 @@
}
}
.scrolling-tabs-container {
position: relative;
.nav-links {
@include scrolling-links();
.fade-right {
@include fade(left, rgba(255, 255, 255, 0.4), $background-color);
right: 0;
}
.fade-left {
@include fade(right, rgba(255, 255, 255, 0.4), $background-color);
left: 0;
}
}
}
.nav-block {
position: relative;
+22 -108
View File
@@ -1,11 +1,3 @@
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
margin-top: 8px;
}
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
@@ -20,12 +12,6 @@
height: 100%;
transition-duration: .3s;
}
.gitlab-text-container-link {
z-index: 1;
position: absolute;
left: 0;
}
}
.sidebar-wrapper {
@@ -50,47 +36,8 @@
.sidebar-wrapper {
.header-logo {
border-bottom: 1px solid transparent;
float: left;
height: $header-height;
width: $sidebar_width;
position: fixed;
z-index: 999;
overflow: hidden;
transition-duration: .3s;
a {
float: left;
height: $header-height;
width: 100%;
padding-left: 22px;
overflow: hidden;
outline: none;
transition-duration: .3s;
img {
width: 36px;
height: 36px;
}
#tanuki-logo, img {
float: left;
}
.gitlab-text-container {
width: 230px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 50px;
font-size: 19px;
line-height: 50px;
font-weight: normal;
}
}
}
padding: 8px 26px;
&:hover {
background-color: #eee;
@@ -98,7 +45,7 @@
}
.sidebar-user {
padding: 7px 22px;
padding: 15px 22px;
position: fixed;
bottom: 40px;
width: $sidebar_width;
@@ -126,8 +73,7 @@
.nav-sidebar {
margin-top: 14 + $header-height;
margin-bottom: 100px;
margin: 22px 0;
transition-duration: .3s;
list-style: none;
overflow: hidden;
@@ -145,13 +91,12 @@
}
a {
padding: 7px 15px;
text-align: center;
padding: 8px;
font-size: $gl-font-size;
line-height: 24px;
color: $gray;
display: block;
text-decoration: none;
padding-left: 23px;
font-weight: normal;
outline: none;
@@ -166,14 +111,12 @@
i {
width: 16px;
color: $gray-light;
margin-right: 13px;
}
.count {
float: right;
background: #eee;
padding: 0 8px;
@include border-radius(6px);
.nav-link-text {
margin-top: 3px;
font-size: 13px;
line-height: 18px;
}
&.back-link i {
@@ -217,25 +160,13 @@
}
.page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
padding-left: 0;
}
padding-left: 0;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0;
}
width: 0;
.header-logo {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0;
}
width: 0;
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
@@ -246,6 +177,10 @@
}
}
#logo {
display: none;
}
.nav-sidebar {
width: $sidebar_collapsed_width;
@@ -261,44 +196,23 @@
}
.collapse-nav a {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0;
}
width: 0;
}
.sidebar-user {
padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0;
padding-left: 0;
padding-right: 0;
}
width: 0;
padding-left: 0;
padding-right: 0;
.username {
display: none;
}
}
}
.layout-nav {
padding-right: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
padding-right: 0;;
}
}
}
.page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
padding-left: $sidebar_width;
@media (max-width: $screen-xs-min) {
padding-left: 0;
@@ -328,7 +242,7 @@
}
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
padding-right: 62px;
padding-right: 90px;
}
@media (min-width: $screen-md-min) {
@@ -5,7 +5,7 @@
padding: 0;
.timeline-entry {
padding: $gl-padding $gl-btn-padding;
padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
@@ -2,7 +2,7 @@
* Layout
*/
$sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$sidebar_width: 90px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
@@ -1,5 +1,15 @@
@import "framework/variables";
// This file is largely copied from `highlight/white.scss`, but modified to
// avoid all descendant selectors (`table td`). This is because the CSS inlining
// we use performs dramatically worse on descendant selectors than the
// alternatives.
// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
//
// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
// preference): plain class selectors, type (element name) selectors, or
// explicit child selectors.
table.code {
width: 100%;
font-family: monospace;
@@ -11,33 +21,162 @@ table.code {
-premailer-cellspacing: 0;
-premailer-width: 100%;
td {
> tr > td {
line-height: $code_line_height;
font-family: monospace;
font-size: $code_font_size;
}
td.diff-line-num {
margin: 0;
padding: 0;
border: none;
background: $background-color;
color: rgba(0, 0, 0, 0.3);
padding: 0 5px;
border-right: 1px solid $border-color;
text-align: right;
min-width: 35px;
max-width: 50px;
width: 35px;
}
&.diff-line-num {
margin: 0;
padding: 0;
border: none;
padding: 0 5px;
border-right: 1px solid;
text-align: right;
min-width: 35px;
max-width: 50px;
width: 35px;
}
td.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
&.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
}
}
}
@import "highlight/white";
.line-numbers, .diff-line-num {
background-color: $background-color;
}
.diff-line-num, .diff-line-num a {
color: $black-transparent;
}
pre.code, .diff-line-num {
border-color: $table-border-gray;
}
.code.white, pre.code, .line_content {
background-color: #fff;
color: #333;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
.line_content {
&.old {
background-color: $line-removed;
> .line > span.idiff, > .line > span > span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
> .line > span.idiff, > .line > span > span.idiff {
background-color: $line-added-dark;
}
}
&.match {
color: $black-transparent;
background-color: $match-line;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
pre > .hll {
background-color: #f8eec7 !important;
}
span.highlight_word {
background-color: #fafe3d !important;
}
.hll { background-color: #f8f8f8 }
.c { color: #998; font-style: italic; }
.err { color: #a61717; background-color: #e3d2d2; }
.k { font-weight: bold; }
.o { font-weight: bold; }
.cm { color: #998; font-style: italic; }
.cp { color: #999; font-weight: bold; }
.c1 { color: #998; font-style: italic; }
.cs { color: #999; font-weight: bold; font-style: italic; }
.gd { color: #000; background-color: #fdd; }
.gd .x { color: #000; background-color: #faa; }
.ge { font-style: italic; }
.gr { color: #a00; }
.gh { color: #999; }
.gi { color: #000; background-color: #dfd; }
.gi .x { color: #000; background-color: #afa; }
.go { color: #888; }
.gp { color: #555; }
.gs { font-weight: bold; }
.gu { color: #800080; font-weight: bold; }
.gt { color: #a00; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
.kt { color: #458; font-weight: bold; }
.m { color: #099; }
.s { color: #d14; }
.n { color: #333; }
.na { color: teal; }
.nb { color: #0086b3; }
.nc { color: #458; font-weight: bold; }
.no { color: teal; }
.ni { color: purple; }
.ne { color: #900; font-weight: bold; }
.nf { color: #900; font-weight: bold; }
.nn { color: #555; }
.nt { color: navy; }
.nv { color: teal; }
.ow { font-weight: bold; }
.w { color: #bbb; }
.mf { color: #099; }
.mh { color: #099; }
.mi { color: #099; }
.mo { color: #099; }
.sb { color: #d14; }
.sc { color: #d14; }
.sd { color: #d14; }
.s2 { color: #d14; }
.se { color: #d14; }
.sh { color: #d14; }
.si { color: #d14; }
.sx { color: #d14; }
.sr { color: #009926; }
.s1 { color: #d14; }
.ss { color: #990073; }
.bp { color: #999; }
.vc { color: teal; }
.vg { color: teal; }
.vi { color: teal; }
.il { color: #099; }
.gc { color: #999; background-color: #eaf2f5; }
+8 -8
View File
@@ -6,19 +6,19 @@ p.details {
font-style: italic;
color: #777
}
.footer p {
.footer > p {
font-size: small;
color: #777
}
pre.commit-message {
white-space: pre-wrap;
}
.file-stats a {
.file-stats > a {
text-decoration: none;
}
.file-stats .new-file {
color: #090;
}
.file-stats .deleted-file {
color: #b00;
> .new-file {
color: #090;
}
> .deleted-file {
color: #b00;
}
}
+9 -6
View File
@@ -1,6 +1,4 @@
.awards {
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
@@ -9,8 +7,6 @@
.emoji-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 3px;
z-index: 1000;
min-width: 160px;
@@ -23,7 +19,12 @@
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
transform-origin: 100% -45px;
}
&.is-visible {
pointer-events: all;
@@ -94,6 +95,7 @@
.award-control {
margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
@@ -107,7 +109,8 @@
}
&.is-loading {
.award-control-icon {
.award-control-icon-normal,
.emoji-icon {
display: none;
}
+1 -6
View File
@@ -3,12 +3,7 @@
background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
@@ -29,8 +29,6 @@
margin-top: 6px;
p {
overflow-x: auto;
&:last-child {
margin-bottom: 0;
}
+32 -1
View File
@@ -51,7 +51,7 @@
.label-row {
.label-name {
display: inline-block;
width: 200px;
width: 170px;
@media (max-width: $screen-xs-min) {
display: block;
@@ -138,3 +138,34 @@
}
}
}
.prioritized-labels {
margin-bottom: 30px;
.add-priority {
display: none;
color: $gray-light;
}
}
.other-labels {
.remove-priority {
display: none;
}
}
.toggle-priority {
display: inline-block;
vertical-align: middle;
button {
border-color: transparent;
padding: 5px 8px;
vertical-align: top;
font-size: 14px;
&:hover {
border-color: transparent;
}
}
}
@@ -79,11 +79,14 @@
}
&.ci-failed,
&.ci-canceled,
&.ci-error {
color: $gl-danger;
}
&.ci-canceled {
color: $gl-gray;
}
a.monospace {
color: inherit;
}
@@ -87,6 +87,39 @@
}
}
.md-header .nav-links {
display: flex;
display: -webkit-flex;
flex-flow: row wrap;
-webkit-flex-flow: row wrap;
width: 100%;
.pull-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
}
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 13px;
@media (max-width: $screen-md-min) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
-webkit-order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form {
padding: $gl-padding-top $gl-padding;
background-color: $white-light;
+56 -2
View File
@@ -69,6 +69,10 @@ ul.notes {
.note-edit-form {
display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
}
}
@@ -116,8 +120,41 @@ ul.notes {
}
}
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
.award-control {
font-size: 13px;
padding: 2px 5px;
}
}
.note-header {
padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
}
@@ -179,6 +216,8 @@ ul.notes {
.discussion-header,
.note-header {
position: relative;
a {
color: inherit;
@@ -215,6 +254,16 @@ ul.notes {
color: $notes-action-color;
}
.note-actions {
position: absolute;
right: 0;
top: 0;
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions {
@media (max-width: $screen-md-max) {
float: none;
@@ -228,8 +277,13 @@ ul.notes {
.note-action-button {
display: inline-block;
margin-left: 10px;
line-height: 24px;
margin-left: 0;
line-height: 20px;
@media (min-width: $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
.fa {
color: $notes-action-color;
+14 -4
View File
@@ -32,6 +32,15 @@
.container-fluid {
position: relative;
@media (min-width: $screen-md-max) {
.row {
display: flex;
-ms-flex-align: center;
-webkit-align-items: center;
-webkit-box-align: center;
}
}
}
.cover-controls {
@@ -57,7 +66,6 @@
max-width: 86px;
min-width: 86px;
padding-right: 0;
margin: 11px 0;
@media (max-width: $screen-md-max) {
padding-left: 0;
@@ -489,9 +497,11 @@ pre.light-well {
margin: 0;
}
.project-show-activity {
.activity-filter-block {
margin-top: -1px;
.activity-filter-block {
.controls {
padding-bottom: 10px;
border-bottom: 1px solid $border-color;
}
}
+3 -3
View File
@@ -28,6 +28,7 @@
}
.search-input {
padding-right: 20px;
border: none;
font-size: 14px;
outline: none;
@@ -47,6 +48,7 @@
display: inline-block;
background-color: $location-badge-bg;
vertical-align: top;
cursor: default;
}
.search-input-container {
@@ -55,7 +57,7 @@
position: relative;
}
.search-location-badge, .search-input-wrap {
.search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
}
@@ -156,13 +158,11 @@
.search-holder {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.search-field-holder {
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
margin-right: 0;
+13 -2
View File
@@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base
end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to profile_two_factor_auth_path
end
end
@@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def browser_supports_u2f?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
end
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
@@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
def u2f_app_id
request.base_url
end
private
def set_default_sort
@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
# Returns nil
def prompt_for_two_factor(user)
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
render 'devise/sessions/two_factor' and return
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
private
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
end
end
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
# Remove any lingering user data from login
session.delete(:otp_user_id)
session.delete(:challenges)
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
challenges = sign_requests.map(&:challenge)
session[:challenges] = challenges
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end
end
@@ -0,0 +1,31 @@
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
name = params.require(:name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true }
end
private
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
else
awardable
end
end
def awardable
raise NotImplementedError
end
end
@@ -1,7 +1,7 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new
def show
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
end
@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed?
if two_factor_authentication_required?
if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.'
flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}."
flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end
end
@qr_code = build_qr_code
setup_u2f_registration
end
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true
current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else
@error = 'Invalid pin code'
@qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
render 'new'
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end
end
@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host
Gitlab.config.gitlab.host
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end
@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
private
def build
@build ||= project.builds.unscoped.find_by!(id: params[:build_id])
@build ||= project.builds.find_by!(id: params[:build_id])
end
def artifacts_file
@@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController
redirect_to namespace_project_branches_path(@project.namespace,
@project), status: 303
end
format.js { render status: status[:return_code] }
format.js { render nothing: true, status: status[:return_code] }
end
end
@@ -26,9 +26,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def show
@builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id)
@commit = @build.commit
@pipeline = @build.pipeline
respond_to do |format|
format.html
@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
@build ||= project.builds.unscoped.find_by!(id: params[:id])
@build ||= project.builds.find_by!(id: params[:id])
end
def build_path(build)
@@ -99,12 +99,12 @@ class Projects::CommitController < Projects::ApplicationController
@commit ||= @project.commit(params[:id])
end
def ci_commits
@ci_commits ||= project.ci_commits.where(sha: commit.sha)
def pipelines
@pipelines ||= project.pipelines.where(sha: commit.sha)
end
def ci_builds
@ci_builds ||= Ci::Build.where(commit: ci_commits)
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
def define_show_vars
@@ -117,8 +117,8 @@ class Projects::CommitController < Projects::ApplicationController
@diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
@statuses = CommitStatus.where(commit: ci_commits)
@builds = Ci::Build.where(commit: ci_commits)
@statuses = CommitStatus.where(pipeline: pipelines)
@builds = Ci::Build.where(pipeline: pipelines)
end
def assign_change_commit_vars(mr_source_branch)
+13 -3
View File
@@ -1,6 +1,7 @@
class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@notes = @issue.notes.with_associations.fresh
@noteable = @issue
respond_to do |format|
@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
respond_to do |format|
format.json do
render json: { notice: "#{result[:count]} issues updated" }
end
end
end
protected
@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController
:issues_ids,
:assignee_id,
:milestone_id,
:state_event
:state_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
)
end
end
+27 -2
View File
@@ -5,13 +5,14 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [
:new, :create, :edit, :update, :generate, :destroy
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
]
respond_to :js, :html
def index
@labels = @project.labels.page(params[:page])
@labels = @project.labels.unprioritized.page(params[:page])
@prioritized_labels = @project.labels.prioritized
respond_to do |format|
format.html
@@ -71,6 +72,30 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
def remove_priority
respond_to do |format|
if label.update_attribute(:priority, nil)
format.json { render json: label }
else
message = label.errors.full_messages.uniq.join('. ')
format.json { render json: { message: message }, status: :unprocessable_entity }
end
end
end
def set_priorities
Label.transaction do
params[:label_ids].each_with_index do |label_id, index|
label = @project.labels.find_by_id(label_id)
label.update_attribute(:priority, index) if label
end
end
respond_to do |format|
format.json { render json: { message: 'success' } }
end
end
protected
def module_enabled
@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled
before_action :merge_request, only: [
@@ -57,9 +58,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json { render json: @merge_request }
format.diff { render text: @merge_request.to_diff }
format.patch { render text: @merge_request.to_patch }
format.json { render json: @merge_request }
format.patch { render text: @merge_request.to_patch }
format.diff do
headers.store(*Gitlab::Workhorse.send_git_diff(@project.repository,
@merge_request.diff_base_commit.id,
@merge_request.last_commit.id))
headers['Content-Disposition'] = 'inline'
head :ok
end
end
end
@@ -119,8 +127,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@@ -190,13 +198,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
if params[:sha] != @merge_request.source_sha
@status = :sha_mismatch
return
end
TodoService.new.merge_merge_request(merge_request, current_user)
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
.execute(@merge_request)
@status = :merge_when_build_succeeds
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@@ -225,10 +238,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
ci_commit = @merge_request.ci_commit
if ci_commit
status = ci_commit.status
coverage = ci_commit.try(:coverage)
pipeline = @merge_request.pipeline
if pipeline
status = pipeline.status
coverage = pipeline.try(:coverage)
status ||= "preparing"
else
@@ -265,6 +278,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
@@ -300,7 +314,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@discussions = @notes.discussions
@noteable = @merge_request
@@ -310,8 +324,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff
@ci_commit = @merge_request.ci_commit
@statuses = @ci_commit.statuses if @ci_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses if @pipeline
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@@ -320,8 +334,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_widget_vars
@ci_commit = @merge_request.ci_commit
@ci_commits = [@ci_commit].compact
@pipeline = @merge_request.pipeline
@pipelines = [@pipeline].compact
closes_issues
end
+14 -28
View File
@@ -1,9 +1,11 @@
class Projects::NotesController < Projects::ApplicationController
include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
before_action :find_current_user_notes, only: [:index]
def index
current_fetched_at = Time.now.to_i
@@ -56,35 +58,12 @@ class Projects::NotesController < Projects::ApplicationController
end
end
def award_toggle
noteable = if note_params[:noteable_type] == "issue"
project.issues.find(note_params[:noteable_id])
else
project.merge_requests.find(note_params[:noteable_id])
end
data = {
author: current_user,
is_award: true,
note: note_params[:note].delete(":")
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private
def note
@note ||= @project.notes.find(params[:id])
end
alias_method :awardable, :note
def note_to_html(note)
render_to_string(
@@ -131,13 +110,20 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_json(note)
if note.valid?
if note.is_a?(AwardEmoji)
{
valid: note.valid?,
award: true,
id: note.id,
name: note.name
}
elsif note.valid?
{
valid: true,
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
award: note.is_award,
award: false,
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
@@ -145,7 +131,7 @@ class Projects::NotesController < Projects::ApplicationController
else
{
valid: false,
award: note.is_award,
award: false,
errors: note.errors
}
end
@@ -7,7 +7,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
all_pipelines = project.ci_commits
all_pipelines = project.pipelines
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def new
@pipeline = project.ci_commits.new(ref: @project.default_branch)
@pipeline = project.pipelines.new(ref: @project.default_branch)
end
def create
@@ -50,7 +50,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def pipeline
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
@pipeline ||= project.pipelines.find_by!(id: params[:id])
end
def commit
+1 -1
View File
@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
emojis: AwardEmoji.urls,
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
+12 -24
View File
@@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
log_audit_event(current_user, with: authenticated_with)
log_audit_event(current_user, with: authentication_method)
end
end
@@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController
end
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end
def find_user
@@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController
find_user.try(:two_factor_enabled?)
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) and return
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor and return
end
else
if user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
end
def auto_sign_in_with_provider
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
@@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
def authentication_method
if user_params[:otp_attempt]
"two-factor"
elsif user_params[:device_response]
"two-factor-via-u2f-device"
else
"standard"
end
end
end
+6 -2
View File
@@ -224,7 +224,7 @@ class IssuableFinder
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end
def by_assignee(items)
@@ -318,7 +318,11 @@ class IssuableFinder
end
def label_names
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
if labels?
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
else
[]
end
end
def current_user_related?
+2 -2
View File
@@ -12,9 +12,9 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
project.issues.find(target_id).notes.nonawards.inc_author
project.issues.find(target_id).notes.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
+13 -1
View File
@@ -30,7 +30,7 @@ class TodosFinder
items = by_state(items)
items = by_type(items)
items
items.reorder(id: :desc)
end
private
@@ -78,6 +78,16 @@ class TodosFinder
@project
end
def projects
return @projects if defined?(@projects)
if project?
@projects = project
else
@projects = ProjectsFinder.new.execute(current_user)
end
end
def type?
type.present? && ['Issue', 'MergeRequest'].include?(type)
end
@@ -105,6 +115,8 @@ class TodosFinder
def by_project(items)
if project?
items = items.where(project: project)
elsif projects
items = items.merge(projects).joins(:project)
end
items
+1 -1
View File
@@ -66,7 +66,7 @@ module AuthHelper
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled &&
!current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
+1 -1
View File
@@ -30,7 +30,7 @@ module ButtonHelper
content_tag :a, protocol,
class: klass,
href: @project.http_url_to_repo,
href: project.http_url_to_repo,
data: {
html: true,
placement: 'right',
+3 -3
View File
@@ -1,7 +1,7 @@
module CiStatusHelper
def ci_status_path(ci_commit)
project = ci_commit.project
builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
def ci_status_path(pipeline)
project = pipeline.project
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
end
def ci_status_with_icon(status, target = nil)
+1 -1
View File
@@ -31,7 +31,7 @@ module GroupsHelper
if group && group.avatar.present?
group.avatar.url
else
'no_group_avatar.png'
image_path('no_group_avatar.png')
end
end
-13
View File
@@ -8,14 +8,6 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
def issuables_count(issuable)
base_issuable_scope(issuable).maximum(:iid)
end
def next_issuable_for(issuable)
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
def multi_label_name(current_labels, default_label)
# current_labels may be a string from before
if current_labels.is_a?(Array)
@@ -45,10 +37,6 @@ module IssuablesHelper
end
end
def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end
def user_dropdown_label(user_id, default_label)
return default_label if user_id.nil?
return "Unassigned" if user_id == "0"
@@ -96,5 +84,4 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
end
+6 -8
View File
@@ -145,16 +145,14 @@ module IssuesHelper
end
end
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.name
end
list.join(", ")
def award_user_list(awards, current_user)
awards.map do |award|
award.user == current_user ? 'me' : award.user.name
end.join(', ')
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
def award_active_class(awards, current_user)
if current_user && awards.find { |a| a.user_id == current_user.id }
"active"
else
""
+19 -3
View File
@@ -31,6 +31,21 @@ module NotificationsHelper
end
end
def notification_description(level)
case level.to_sym
when :participating
'You will only receive notifications from related resources'
when :mention
'You will receive notifications only for comments in which you were @mentioned'
when :watch
'You will receive notifications for any activity'
when :disabled
'You will not get any notifications via email'
when :global
'Use your global notification setting'
end
end
def notification_list_item(level, setting)
title = notification_title(level)
@@ -39,9 +54,10 @@ module NotificationsHelper
notification_title: title
}
content_tag(:li, class: ('active' if setting.level == level)) do
link_to '#', class: 'update-notification', data: data do
notification_icon(level, title)
content_tag(:li, role: "menuitem") do
link_to '#', class: "update-notification #{('is-active' if setting.level == level)}", data: data do
link_output = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
link_output << content_tag(:span, notification_description(level), class: 'dropdown-menu-inner-content')
end
end
end
+10 -1
View File
@@ -14,7 +14,8 @@ module SortingHelper
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
sort_value_upvotes => sort_title_upvotes
sort_value_upvotes => sort_title_upvotes,
sort_value_priority => sort_title_priority
}
end
@@ -28,6 +29,10 @@ module SortingHelper
}
end
def sort_title_priority
'Priority'
end
def sort_title_oldest_updated
'Oldest updated'
end
@@ -84,6 +89,10 @@ module SortingHelper
'Most popular'
end
def sort_value_priority
'priority'
end
def sort_value_oldest_updated
'updated_asc'
end
+3 -1
View File
@@ -17,7 +17,9 @@ module TodosHelper
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
link_to "#{target} #{todo.target_reference}", todo_target_path(todo),
class: 'has-tooltip',
title: todo.target.title
end
def todo_target_path(todo)
+26
View File
@@ -0,0 +1,26 @@
class AwardEmoji < ActiveRecord::Base
DOWNVOTE_NAME = "thumbsdown".freeze
UPVOTE_NAME = "thumbsup".freeze
include Participable
belongs_to :awardable, polymorphic: true
belongs_to :user
validates :awardable, :user, presence: true
validates :name, presence: true, inclusion: { in: Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
def downvote?
self.name == DOWNVOTE_NAME
end
def upvote?
self.name == UPVOTE_NAME
end
end
+13 -12
View File
@@ -45,8 +45,8 @@ module Ci
new_build.options = build.options
new_build.commands = build.commands
new_build.tag_list = build.tag_list
new_build.gl_project_id = build.gl_project_id
new_build.commit_id = build.commit_id
new_build.project = build.project
new_build.pipeline = build.pipeline
new_build.name = build.name
new_build.allow_failure = build.allow_failure
new_build.stage = build.stage
@@ -66,7 +66,7 @@ module Ci
# We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
around_transition any => [:success, :failed, :canceled] do |build, block|
block.call
build.commit.create_next_builds(build) if build.commit
build.pipeline.create_next_builds(build) if build.pipeline
end
after_transition any => [:success, :failed, :canceled] do |build|
@@ -80,7 +80,7 @@ module Ci
end
def retried?
!self.commit.statuses.latest.include?(self)
!self.pipeline.statuses.latest.include?(self)
end
def retry
@@ -89,7 +89,7 @@ module Ci
def depends_on_builds
# Get builds of the same type
latest_builds = self.commit.builds.latest
latest_builds = self.pipeline.builds.latest
# Return builds from previous stages
latest_builds.where('stage_idx < ?', stage_idx)
@@ -114,16 +114,16 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: commit.gl_project_id)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
merge_request.commits.any? { |ci| ci.id == commit.sha }
merge_request.commits.any? { |ci| ci.id == pipeline.sha }
end
end
def project_id
commit.project.id
pipeline.project_id
end
def project_name
@@ -313,6 +313,7 @@ module Ci
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
project.running_or_pending_build_count(force: true)
end
def artifacts?
@@ -359,8 +360,8 @@ module Ci
end
def global_yaml_variables
if commit.config_processor
commit.config_processor.global_variables.map do |key, value|
if pipeline.config_processor
pipeline.config_processor.global_variables.map do |key, value|
{ key: key, value: value, public: true }
end
else
@@ -369,8 +370,8 @@ module Ci
end
def job_yaml_variables
if commit.config_processor
commit.config_processor.job_variables(name).map do |key, value|
if pipeline.config_processor
pipeline.config_processor.job_variables(name).map do |key, value|
{ key: key, value: value, public: true }
end
else
@@ -1,12 +1,14 @@
module Ci
class Commit < ActiveRecord::Base
class Pipeline < ActiveRecord::Base
extend Ci::Model
include Statuseable
self.table_name = 'ci_commits'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
has_many :statuses, class_name: 'CommitStatus'
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha
validates_presence_of :status
@@ -21,7 +23,7 @@ module Ci
def self.stages
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(commit: pluck(:id)).stages
CommitStatus.where(pipeline: pluck(:id)).stages
end
def project_id
@@ -47,7 +49,7 @@ module Ci
end
def short_sha
Ci::Commit.truncate_sha(sha)
Ci::Pipeline.truncate_sha(sha)
end
def commit_data
+1 -1
View File
@@ -3,7 +3,7 @@ module Ci
extend Ci::Model
belongs_to :trigger, class_name: 'Ci::Trigger'
belongs_to :commit, class_name: 'Ci::Commit'
belongs_to :commit, class_name: 'Ci::Pipeline', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build'
serialize :variables
+3 -3
View File
@@ -214,13 +214,13 @@ class Commit
@raw.short_id(7)
end
def ci_commits
@ci_commits ||= project.ci_commits.where(sha: sha)
def pipelines
@pipeline ||= project.pipelines.where(sha: sha)
end
def status
return @status if defined?(@status)
@status ||= ci_commits.status
@status ||= pipelines.status
end
def revert_branch_name
+6 -6
View File
@@ -4,10 +4,10 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
belongs_to :commit, class_name: 'Ci::Commit', touch: true
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
validates :commit, presence: true
validates :pipeline, presence: true
validates_presence_of :name
@@ -44,18 +44,18 @@ class CommitStatus < ActiveRecord::Base
end
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
end
end
delegate :sha, :short_sha, to: :commit
delegate :sha, :short_sha, to: :pipeline
def before_sha
commit.before_sha || Gitlab::Git::BLANK_SHA
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
def self.stages
+81
View File
@@ -0,0 +1,81 @@
module Awardable
extend ActiveSupport::Concern
included do
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji
end
end
module ClassMethods
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
def order_downvotes_desc
order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
end
def order_votes_desc(emoji_name)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
awards_table[:awardable_id].eq(awardable_table[:id]).and(
awards_table[:awardable_type].eq(self.name).and(
awards_table[:name].eq(emoji_name)
)
)
).join_sources
joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
end
end
def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
end
awards
end
def downvotes
award_emoji.downvotes.count
end
def upvotes
award_emoji.upvotes.count
end
def emoji_awardable?
true
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: name, user: current_user)
end
def remove_award_emoji(name, current_user)
award_emoji.where(name: name, user: current_user).destroy_all
end
def toggle_award_emoji(emoji_name, current_user)
if awarded_emoji?(emoji_name, current_user)
remove_award_emoji(emoji_name, current_user)
else
create_award_emoji(emoji_name, current_user)
end
end
end
+33 -34
View File
@@ -10,6 +10,7 @@ module Issuable
include Mentionable
include Subscribable
include StripAttribute
include Awardable
included do
belongs_to :author, class_name: "User"
@@ -68,6 +69,14 @@ module Issuable
strip_attributes :title
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee
User.find(assignee_id_was).update_cache_counts if assignee_id_was
assignee.update_cache_counts if assignee
end
end
module ClassMethods
@@ -96,38 +105,22 @@ module Issuable
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end
def sort(method)
def sort(method, excluded_labels: [])
case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
else
order_by(method)
end
end
def order_downvotes_desc
order_votes_desc('thumbsdown')
end
def order_upvotes_desc
order_votes_desc('thumbsup')
end
def order_votes_desc(award_emoji_name)
issuable_table = self.arel_table
note_table = Note.arel_table
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
note_table[:noteable_id].eq(issuable_table[:id]).and(
note_table[:noteable_type].eq(self.name).and(
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
)
)
).join_sources
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
def order_labels_priority(excluded_labels: [])
select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
def with_label(title, sort = nil)
@@ -153,6 +146,20 @@ module Issuable
grouping_columns
end
private
def highest_label_priority(excluded_labels)
query = Label.select(Label.arel_table[:priority].minimum).
joins(:label_links).
where(label_links: { target_type: name }).
where("label_links.target_id = #{table_name}.id").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?
query
end
end
def today?
@@ -163,10 +170,6 @@ module Issuable
today? && created_at == updated_at
end
def is_assigned?
!!assignee_id
end
def is_being_reassigned?
assignee_id_changed?
end
@@ -175,14 +178,6 @@ module Issuable
opened? || reopened?
end
def downvotes
notes.awards.where(note: "thumbsdown").count
end
def upvotes
notes.awards.where(note: "thumbsup").count
end
def user_notes_count
notes.user.count
end
@@ -205,6 +200,10 @@ module Issuable
hook_data
end
def labels_array
labels.to_a
end
def label_names
labels.order('title ASC').pluck(:title)
end
+2 -2
View File
@@ -75,10 +75,10 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
def self.sort(method)
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
when 'due_date_desc' then order_due_date_desc
else
super
end
+14
View File
@@ -26,10 +26,20 @@ class Label < ActiveRecord::Base
format: { with: /\A[^&\?,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
default_scope { order(title: :asc) }
scope :templates, -> { where(template: true) }
def self.prioritized
where.not(priority: nil).reorder(:priority, :title)
end
def self.unprioritized
where(priority: nil)
end
alias_attribute :name, :title
def self.reference_prefix
@@ -118,4 +128,8 @@ class Label < ActiveRecord::Base
id
end
end
def nullify_priority
self.priority = nil if priority.blank?
end
end
+4
View File
@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
@active
end
def award_emoji_supported?
false
end
private
def find_diff
+2 -9
View File
@@ -313,13 +313,6 @@ class MergeRequest < ActiveRecord::Base
)
end
# Returns the raw diff for this merge request
#
# see "git diff"
def to_diff
target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
@@ -579,8 +572,8 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
def pipeline
@pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project
end
def diff_refs
+8 -45
View File
@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base
include Gitlab::CurrentSettings
include Participable
include Mentionable
include Awardable
default_value_for :system, false
@@ -21,11 +22,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
before_validation :set_award!
validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -43,8 +41,6 @@ class Note < ActiveRecord::Base
mount_uploader :attachment, AttachmentUploader
# Scopes
scope :awards, ->{ where(is_award: true) }
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
@@ -109,19 +105,6 @@ class Note < ActiveRecord::Base
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
end
end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end
def cross_reference?
@@ -205,44 +188,24 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
def downvote?
is_award && note == "thumbsdown"
end
def upvote?
is_award && note == "thumbsup"
end
def editable?
!system? && !is_award
!system?
end
def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty?
end
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
def award_emoji?
award_emoji_supported? && contains_emoji_only?
end
private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported?
(for_issue? || for_merge_request?) && !diff_note?
def award_emoji_supported?
noteable.is_a?(Awardable)
end
def contains_emoji_only?
@@ -251,6 +214,6 @@ class Note < ActiveRecord::Base
def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
AwardEmoji.normilize_emoji_name(original_name)
Gitlab::AwardEmoji.normalize_emoji_name(original_name)
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class NotificationSetting < ActiveRecord::Base
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 }
default_value_for :level, NotificationSetting.levels[:global]
+23 -5
View File
@@ -119,7 +119,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
@@ -930,12 +930,12 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
def ci_commit(sha, ref)
ci_commits.order(id: :desc).find_by(sha: sha, ref: ref)
def pipeline(sha, ref)
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
def ensure_ci_commit(sha, ref)
ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref)
def ensure_pipeline(sha, ref)
pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref)
end
def enable_ci
@@ -1011,4 +1011,22 @@ class Project < ActiveRecord::Base
update_attribute(:pending_delete, true)
end
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
end
end
def mark_import_as_failed(error_message)
original_errors = errors.dup
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
import_fail
update_column(:import_error, sanitized_message)
rescue ActiveRecord::ActiveRecordError => e
Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
ensure
@errors = original_errors
end
end
+1 -1
View File
@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient)
end
channels.reject! &:nil?
channels.reject!(&:nil?)
end
def format_channel(recipient)
+40
View File
@@ -0,0 +1,40 @@
# Registration information for U2F (universal 2nd factor) devices, like Yubikeys
class U2fRegistration < ActiveRecord::Base
belongs_to :user
def self.register(user, app_id, json_response, challenges)
u2f = U2F::U2F.new(app_id)
registration = self.new
begin
response = U2F::RegisterResponse.load_from_json(json_response)
registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
user: user)
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
registration.errors.add(:base, e.message)
end
registration
end
def self.authenticate(user, app_id, json_response, challenges)
response = U2F::SignResponse.load_from_json(json_response)
registration = user.u2f_registrations.find_by_key_handle(response.key_handle)
u2f = U2F::U2F.new(app_id)
if registration
u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter)
registration.update(counter: response.counter)
true
end
rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
false
end
end
+52 -11
View File
@@ -27,7 +27,6 @@ class User < ActiveRecord::Base
devise :two_factor_authenticatable,
otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
alias_attribute :two_factor_enabled, :otp_required_for_login
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
@@ -51,6 +50,7 @@ class User < ActiveRecord::Base
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
#
# Validations
@@ -174,8 +175,16 @@ class User < ActiveRecord::Base
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
scope :with_two_factor, -> { where(two_factor_enabled: true) }
scope :without_two_factor, -> { where(two_factor_enabled: false) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NULL AND otp_required_for_login = ?", false)
end
#
# Class methods
@@ -322,14 +331,29 @@ class User < ActiveRecord::Base
end
def disable_two_factor!
update_attributes(
two_factor_enabled: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
transaction do
update_attributes(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
self.u2f_registrations.destroy_all
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
end
def two_factor_otp_enabled?
self.otp_required_for_login?
end
def two_factor_u2f_enabled?
self.u2f_registrations.exists?
end
def namespace_uniq
@@ -776,6 +800,23 @@ class User < ActiveRecord::Base
notification_settings.find_or_initialize_by(source: source)
end
def assigned_open_merge_request_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
assigned_merge_requests.opened.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
assigned_issues.opened.count
end
end
def update_cache_counts
assigned_open_merge_request_count(force: true)
assigned_open_issues_count(force: true)
end
private
def projects_union
+10 -10
View File
@@ -1,11 +1,11 @@
module Ci
class CreateBuildsService
def initialize(commit)
@commit = commit
def initialize(pipeline)
@pipeline = pipeline
end
def execute(stage, user, status, trigger_request = nil)
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request)
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
@@ -21,8 +21,8 @@ module Ci
builds_attrs.map do |build_attrs|
# don't create the same build twice
unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag,
trigger_request: trigger_request, name: build_attrs[:name])
unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag,
trigger_request: trigger_request, name: build_attrs[:name])
build_attrs.slice!(:name,
:commands,
:tag_list,
@@ -31,13 +31,13 @@ module Ci
:stage,
:stage_idx)
build_attrs.merge!(ref: @commit.ref,
tag: @commit.tag,
build_attrs.merge!(ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: trigger_request,
user: user,
project: @commit.project)
project: @pipeline.project)
@commit.builds.create!(build_attrs)
@pipeline.builds.create!(build_attrs)
end
end
end
@@ -45,7 +45,7 @@ module Ci
private
def config_processor
@config_processor ||= @commit.config_processor
@config_processor ||= @pipeline.config_processor
end
end
end
+2 -2
View File
@@ -1,7 +1,7 @@
module Ci
class CreatePipelineService < BaseService
def execute
pipeline = project.ci_commits.new(params)
pipeline = project.pipelines.new(params)
unless ref_names.include?(params[:ref])
pipeline.errors.add(:base, 'Reference not found')
@@ -19,7 +19,7 @@ module Ci
end
begin
Ci::Commit.transaction do
Ci::Pipeline.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
@@ -7,14 +7,14 @@ module Ci
# check if ref is tag
tag = project.repository.find_tag(ref).present?
ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag)
pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
trigger_request = trigger.trigger_requests.create!(
variables: variables,
commit: ci_commit,
commit: pipeline,
)
if ci_commit.create_builds(nil, trigger_request)
if pipeline.create_builds(nil, trigger_request)
trigger_request
end
end
+3 -3
View File
@@ -3,9 +3,9 @@ module Ci
def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref])
ci_commits = project.ci_commits.where(sha: sha)
ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref]
image_name = image_for_status(ci_commits.status)
pipelines = project.pipelines.where(sha: sha)
pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
image_name = image_for_status(pipelines.status)
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(path: image_path, name: image_name)
+9 -9
View File
@@ -18,23 +18,23 @@ class CreateCommitBuildsService
return false
end
commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file
# Skip creating pipeline when no gitlab-ci.yml is found
unless pipeline.ci_yaml_file
return false
end
# Create a new ci_commit
commit.save!
# Create a new pipeline
pipeline.save!
# Skip creating builds for commits that have [ci skip]
unless commit.skip_ci?
unless pipeline.skip_ci?
# Create builds for commit
commit.create_builds(user)
pipeline.create_builds(user)
end
commit.touch
commit
pipeline.touch
pipeline
end
end

Some files were not shown because too many files have changed in this diff Show More