Commit ae2df96b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'rc/ce-to-ee-monday' into 'master'

CE Upstream - Monday

Closes gitlab-ce#29897

See merge request !1517
parents e56f1dc3 376fc805
...@@ -15,6 +15,8 @@ variables: ...@@ -15,6 +15,8 @@ variables:
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json
# This hack is needed to make ES not that memory hungry # This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m" ES_JAVA_OPTS: "-Xms600m -Xmx600m"
...@@ -23,7 +25,7 @@ before_script: ...@@ -23,7 +25,7 @@ before_script:
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS' - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS'
- retry gem install knapsack - retry gem install knapsack fog-aws mime-types
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
- '[ "$SETUP_DB" != "true" ] || bundle exec rake geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate' - '[ "$SETUP_DB" != "true" ] || bundle exec rake geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate'
...@@ -43,14 +45,15 @@ stages: ...@@ -43,14 +45,15 @@ stages:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache: cache:
key: "knapsack" key: "knapsack"
paths: paths:
- knapsack/ - knapsack/
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
- knapsack/ - knapsack/
.use-db: &use-db .use-db: &use-db
services: services:
...@@ -66,17 +69,17 @@ stages: ...@@ -66,17 +69,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
...@@ -86,28 +89,44 @@ stages: ...@@ -86,28 +89,44 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
<<: *knapsack-state <<: *knapsack-state
<<: *dedicated-runner <<: *dedicated-runner
stage: prepare stage: prepare
script: script:
- mkdir -p knapsack/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
setup-test-env: setup-test-env:
<<: *use-db <<: *use-db
...@@ -127,20 +146,6 @@ setup-test-env: ...@@ -127,20 +146,6 @@ setup-test-env:
- public/assets - public/assets
- tmp/tests - tmp/tests
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
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
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
rspec 0 20: *rspec-knapsack rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack rspec 2 20: *rspec-knapsack
...@@ -266,6 +271,7 @@ rake karma: ...@@ -266,6 +271,7 @@ rake karma:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
script: script:
- bundle exec rake karma - bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts: artifacts:
name: coverage-javascript name: coverage-javascript
expire_in: 31d expire_in: 31d
...@@ -297,7 +303,7 @@ bundler:audit: ...@@ -297,7 +303,7 @@ bundler:audit:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- "bundle exec bundle-audit check --update" - "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
migration paths: migration paths:
stage: test stage: test
...@@ -331,6 +337,7 @@ coverage: ...@@ -331,6 +337,7 @@ coverage:
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
script: script:
- bundle exec scripts/merge-simplecov - bundle exec scripts/merge-simplecov
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts: artifacts:
name: coverage name: coverage
expire_in: 31d expire_in: 31d
......
...@@ -255,7 +255,7 @@ gem 'base32', '~> 0.3.0' ...@@ -255,7 +255,7 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0" gem "gitlab-license", "~> 1.0"
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 2.0.0' gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
...@@ -268,15 +268,14 @@ end ...@@ -268,15 +268,14 @@ end
group :development do group :development do
gem 'foreman', '~> 0.78.0' gem 'foreman', '~> 0.78.0'
gem 'brakeman', '~> 3.4.0', require: false gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.2.0', require: false gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0'
# Better errors handler # Better errors handler
gem 'better_errors', '~> 1.0.1' gem 'better_errors', '~> 2.1.0'
gem 'binding_of_caller', '~> 0.7.2' gem 'binding_of_caller', '~> 0.7.2'
# thin instead webrick # thin instead webrick
...@@ -308,7 +307,7 @@ group :development, :test do ...@@ -308,7 +307,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
gem 'spring', '~> 1.7.0' gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
...@@ -316,8 +315,8 @@ group :development, :test do ...@@ -316,8 +315,8 @@ group :development, :test do
gem 'rubocop-rspec', '~> 1.12.0', require: false gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '0.12.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.6.1', require: false gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
...@@ -334,7 +333,7 @@ group :test do ...@@ -334,7 +333,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2' gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.21.0' gem 'webmock', '~> 1.24.0'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
......
...@@ -83,19 +83,20 @@ GEM ...@@ -83,19 +83,20 @@ GEM
base32 (0.3.2) base32 (0.3.2)
bcrypt (3.1.11) bcrypt (3.1.11)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
better_errors (1.0.1) better_errors (2.1.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.3.5) bindata (2.3.5)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.4.1) brakeman (3.6.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.3) builder (3.2.3)
bullet (5.2.0) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0) bundler-audit (0.5.0)
...@@ -109,7 +110,7 @@ GEM ...@@ -109,7 +110,7 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (~> 2.0)
capybara-screenshot (1.0.11) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (0.11.2) carrierwave (0.11.2)
...@@ -125,7 +126,7 @@ GEM ...@@ -125,7 +126,7 @@ GEM
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
cliver (0.3.2) cliver (0.3.2)
coderay (1.1.0) coderay (1.1.1)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1) coffee-rails (4.1.1)
...@@ -224,7 +225,9 @@ GEM ...@@ -224,7 +225,9 @@ GEM
multi_json multi_json
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.10) ffi (1.9.10)
flay (2.6.1) flay (2.8.1)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
flowdock (0.7.1) flowdock (0.7.1)
...@@ -375,6 +378,7 @@ GEM ...@@ -375,6 +378,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashdiff (0.3.2)
hashie (3.5.5) hashie (3.5.5)
health_check (2.6.0) health_check (2.6.0)
rails (>= 4.0) rails (>= 4.0)
...@@ -554,6 +558,7 @@ GEM ...@@ -554,6 +558,7 @@ GEM
activerecord (>= 4.0, < 5.1) activerecord (>= 4.0, < 5.1)
parser (2.4.0.0) parser (2.4.0.0)
ast (~> 2.2) ast (~> 2.2)
path_expander (1.0.1)
pg (0.18.4) pg (0.18.4)
poltergeist (1.9.0) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
...@@ -568,14 +573,14 @@ GEM ...@@ -568,14 +573,14 @@ GEM
premailer-rails (1.9.2) premailer-rails (1.9.2)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pry (0.10.3) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
pry-byebug (3.4.1) pry-byebug (3.4.2)
byebug (~> 9.0) byebug (~> 9.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.4) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.5) rack (1.6.5)
...@@ -707,7 +712,7 @@ GEM ...@@ -707,7 +712,7 @@ GEM
ruby-progressbar (1.8.1) ruby-progressbar (1.8.1)
ruby-saml (1.4.1) ruby-saml (1.4.1)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
ruby_parser (3.8.2) ruby_parser (3.8.4)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
...@@ -736,10 +741,10 @@ GEM ...@@ -736,10 +741,10 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (2.0.2) sentry-raven (2.4.0)
faraday (>= 0.7.6, < 0.10.x) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.7.0) sexp_processor (4.8.0)
sham_rack (1.3.6) sham_rack (1.3.6)
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
...@@ -760,7 +765,7 @@ GEM ...@@ -760,7 +765,7 @@ GEM
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.5) jwt (~> 1.5)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.12.0) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
...@@ -777,7 +782,8 @@ GEM ...@@ -777,7 +782,8 @@ GEM
spinach (>= 0.4) spinach (>= 0.4)
spinach-rerun-reporter (0.0.2) spinach-rerun-reporter (0.0.2)
spinach (~> 0.8) spinach (~> 0.8)
spring (1.7.2) spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-spinach (1.1.0) spring-commands-spinach (1.1.0)
...@@ -849,14 +855,10 @@ GEM ...@@ -849,14 +855,10 @@ GEM
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
web-console (2.3.0) webmock (1.24.6)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
webmock (1.21.0)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.9) webpack-rails (0.9.9)
rails (>= 3.2.0) rails (>= 3.2.0)
websocket-driver (0.6.3) websocket-driver (0.6.3)
...@@ -891,12 +893,12 @@ DEPENDENCIES ...@@ -891,12 +893,12 @@ DEPENDENCIES
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 1.0.1) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.4.0) brakeman (~> 3.6.0)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.2.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.6.2) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
...@@ -926,7 +928,7 @@ DEPENDENCIES ...@@ -926,7 +928,7 @@ DEPENDENCIES
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
faraday_middleware-aws-signers-v4 faraday_middleware-aws-signers-v4
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.6.1) flay (~> 2.8.0)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.40) fog-core (~> 1.40)
fog-google (~> 0.5) fog-google (~> 0.5)
...@@ -1036,18 +1038,18 @@ DEPENDENCIES ...@@ -1036,18 +1038,18 @@ DEPENDENCIES
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 2.0.0) sentry-raven (~> 2.4.0)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7) sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0) simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1) slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2) spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0) spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0) spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0) sprockets (~> 3.7.0)
...@@ -1068,8 +1070,7 @@ DEPENDENCIES ...@@ -1068,8 +1070,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0) version_sorter (~> 2.1.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
web-console (~> 2.0) webmock (~> 1.24.0)
webmock (~> 1.21.0)
webpack-rails (~> 0.9.9) webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
# GitLab # GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ee/commits/master) [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript
## Canonical source ## Canonical source
The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
......
...@@ -94,7 +94,7 @@ $(() => { ...@@ -94,7 +94,7 @@ $(() => {
resp.json().forEach((board) => { resp.json().forEach((board) => {
const list = Store.addList(board); const list = Store.addList(board);
if (list.type === 'done') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
} }
}); });
......
...@@ -14,6 +14,8 @@ export default { ...@@ -14,6 +14,8 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store); this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens(); this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
}, },
destroyed() { destroyed() {
this.filteredSearch.cleanup(); this.filteredSearch.cleanup();
......
...@@ -53,7 +53,7 @@ import Vue from 'vue'; ...@@ -53,7 +53,7 @@ import Vue from 'vue';
template: ` template: `
<div <div
class="block list" class="block list"
v-if="list.type !== 'done'"> v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -29,6 +29,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -29,6 +29,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
[].forEach.call(tokens, (el) => { [].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}); });
this.filteredSearchInput.value = '';
} }
updateTokens() { updateTokens() {
......
...@@ -10,7 +10,7 @@ class List { ...@@ -10,7 +10,7 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; this.type = obj.list_type;
this.preset = ['done', 'blank'].indexOf(this.type) > -1; this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
......
...@@ -50,7 +50,7 @@ import Cookies from 'js-cookie'; ...@@ -50,7 +50,7 @@ import Cookies from 'js-cookie';
}, },
shouldAddBlankState () { shouldAddBlankState () {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'done')[0]); return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
}, },
addBlankState () { addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
...@@ -103,7 +103,7 @@ import Cookies from 'js-cookie'; ...@@ -103,7 +103,7 @@ import Cookies from 'js-cookie';
issueTo.removeLabel(listFrom.label); issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'done') { if (listTo.type === 'closed') {
issueLists.forEach((list) => { issueLists.forEach((list) => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
......
/* eslint-disable no-new*/
/* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub'; import eventHub from '../../vue_pipelines_index/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
...@@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor'; ...@@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('pipelines-table', { export default Vue.component('pipelines-table', {
components: { components: {
'pipelines-table-component': PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState,
'empty-state': EmptyState,
}, },
/** /**
...@@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', { ...@@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', {
return { return {
endpoint: pipelinesTableData.endpoint, endpoint: pipelinesTableData.endpoint,
helpPagePath: pipelinesTableData.helpPagePath,
store, store,
state: store.state, state: store.state,
isLoading: false, isLoading: false,
hasError: false,
}; };
}, },
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
shouldRenderEmptyState() {
return !this.state.pipelines.length && !this.isLoading;
},
},
/** /**
* When the component is about to be mounted, tell the service to fetch the data * When the component is about to be mounted, tell the service to fetch the data
* *
...@@ -80,26 +94,25 @@ export default Vue.component('pipelines-table', { ...@@ -80,26 +94,25 @@ export default Vue.component('pipelines-table', {
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
this.hasError = true;
this.isLoading = false; this.isLoading = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
}); });
}, },
}, },
template: ` template: `
<div class="pipelines"> <div class="content-list pipelines">
<div class="realtime-loading" v-if="isLoading"> <div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
</div> </div>
<div class="blank-state blank-state-no-icon" <empty-state
v-if="!isLoading && state.pipelines.length === 0"> v-if="shouldRenderEmptyState"
<h2 class="blank-state-title js-blank-state-title"> :help-page-path="helpPagePath" />
No pipelines to show
</h2> <error-state v-if="shouldRenderErrorState" />
</div>
<div class="table-holder pipelines" <div class="table-holder"
v-if="!isLoading && state.pipelines.length > 0"> v-if="!isLoading && state.pipelines.length > 0">
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
......
// ECMAScript polyfills // ECMAScript polyfills
import 'core-js/fn/array/find'; import 'core-js/fn/array/find';
import 'core-js/fn/array/from';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/promise'; import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point'; import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
// Browser polyfills // Browser polyfills
import './polyfills/custom_event'; import './polyfills/custom_event';
......
export default {
props: {
count: {
type: Number,
required: true,
},
},
template: `
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
`,
};
...@@ -14,6 +14,7 @@ import Vue from 'vue'; ...@@ -14,6 +14,7 @@ import Vue from 'vue';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
......
...@@ -14,6 +14,7 @@ import Vue from 'vue'; ...@@ -14,6 +14,7 @@ import Vue from 'vue';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
......
...@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg'; ...@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<span v-if="items.length === 50" class="events-info pull-right"> <limit-warning :count="items.length" />
<i class="fa fa-warning has-tooltip"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item"> <li v-for="commit in items" class="stage-event-item">
......
...@@ -14,6 +14,7 @@ import Vue from 'vue'; ...@@ -14,6 +14,7 @@ import Vue from 'vue';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
......
...@@ -14,6 +14,7 @@ import Vue from 'vue'; ...@@ -14,6 +14,7 @@ import Vue from 'vue';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
......
...@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg'; ...@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component"> <li v-for="build in items" class="stage-event-item item-build-component">
......
...@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg'; ...@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div> <div>
<div class="events-description"> <div class="events-description">
{{ stage.description }} {{ stage.description }}
<limit-warning :count="items.length" />
</div> </div>
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component"> <li v-for="build in items" class="stage-event-item item-build-component">
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component'); require('./components/stage_code_component');
require('./components/stage_issue_component'); require('./components/stage_issue_component');
...@@ -130,5 +131,6 @@ $(() => { ...@@ -130,5 +131,6 @@ $(() => {
}); });
// Register global components // Register global components
Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
}); });
...@@ -33,11 +33,7 @@ class Diff { ...@@ -33,11 +33,7 @@ class Diff {
handleClickUnfold(e) { handleClickUnfold(e) {
const $target = $(e.target); const $target = $(e.target);
// current babel config relies on iterators implementation, so we cannot simply do: const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
// const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const ref = this.lineNumbers($target.parent());
const oldLineNumber = ref[0];
const newLineNumber = ref[1];
const offset = newLineNumber - oldLineNumber; const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom'); const bottom = $target.hasClass('js-unfold-bottom');
let since; let since;
...@@ -105,10 +101,11 @@ class Diff { ...@@ -105,10 +101,11 @@ class Diff {
} }
lineNumbers(line) { lineNumbers(line) {
if (!line.children().length) { const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
return [0, 0]; return [0, 0];
} }
return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
} }
highlightSelectedLine() { highlightSelectedLine() {
......
...@@ -56,10 +56,12 @@ require('../window')(function(w){ ...@@ -56,10 +56,12 @@ require('../window')(function(w){
this.hookInput = hookInput; this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
}, },
destroy: function destroy(){ destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
} }
}; };
}); });
......
const GROUP_LIMIT = 2;
import _ from 'underscore';
export default class GroupName { export default class GroupName {
constructor() { constructor() {
this.titleContainer = document.querySelector('.title'); this.titleContainer = document.querySelector('.title-container');
this.groups = document.querySelectorAll('.group-path'); this.title = document.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title'); this.groupTitle = document.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path');
this.toggle = null; this.toggle = null;
this.isHidden = false; this.isHidden = false;
this.init(); this.init();
} }
init() { init() {
if (this.groups.length > GROUP_LIMIT) { if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable'); this.groups[this.groups.length - 1].classList.remove('hidable');
this.addToggle(); this.toggleHandler();
window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
} }
this.render(); this.render();
} }
addToggle() { toggleHandler() {
const header = document.querySelector('.header-content'); if (this.titleWidth > this.titleContainer.offsetWidth) {
if (!this.toggle) this.createToggle();
this.showToggle();
} else if (this.toggle) {
this.hideToggle();
}
}
createToggle() {
this.toggle = document.createElement('button'); this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle'; this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...'; this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this)); this.toggle.addEventListener('click', this.toggleGroups.bind(this));
header.insertBefore(this.toggle, this.titleContainer); this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups(); this.toggleGroups();
} }
showToggle() {
this.title.classList.add('wrap');
this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('is-hidden');
}
hideToggle() {
this.title.classList.remove('wrap');
this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
}
toggleGroups() { toggleGroups() {
this.isHidden = !this.isHidden; this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden'); this.groupTitle.classList.toggle('is-hidden');
} }
render() { render() {
this.titleContainer.classList.remove('initializing'); this.title.classList.remove('initializing');
} }
} }
...@@ -36,20 +36,21 @@ export default class Poll { ...@@ -36,20 +36,21 @@ export default class Poll {
this.options.data = options.data || {}; this.options.data = options.data || {};
this.intervalHeader = 'POLL-INTERVAL'; this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
this.canPoll = true;
} }
checkConditions(response) { checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers); const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader]; const pollInterval = headers[this.intervalHeader];
if (pollInterval > 0 && response.status === httpStatusCodes.OK) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.options.successCallback(response); this.timeoutID = setTimeout(() => {
setTimeout(() => {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
} else {
this.options.successCallback(response);
} }
this.options.successCallback(response);
} }
makeRequest() { makeRequest() {
...@@ -59,4 +60,14 @@ export default class Poll { ...@@ -59,4 +60,14 @@ export default class Poll {
.then(response => this.checkConditions(response)) .then(response => this.checkConditions(response))
.catch(error => errorCallback(error)); .catch(error => errorCallback(error));
} }
/**
* Stops the polling recursive chain
* and guarantees if the timeout is already running it won't make another request by
* cancelling the previously established timeout.
*/
stop() {
this.canPoll = false;
clearTimeout(this.timeoutID);
}
} }
...@@ -127,9 +127,6 @@ require('./flash'); ...@@ -127,9 +127,6 @@ require('./flash');
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
} }
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: 0,
});
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
if (this.pipelinesLoaded) { if (this.pipelinesLoaded) {
return; return;
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
bindEvents() { bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm); $('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername); $('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs); $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
......
...@@ -200,7 +200,7 @@ import Cookies from 'js-cookie'; ...@@ -200,7 +200,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.setSidebarHeight = function() { Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar'); const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $('body').scrollTop(); const diff = $navHeight - $(window).scrollTop();
if (diff > 0) { if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff); $rightSidebar.outerHeight($(window).height() - diff);
} else { } else {
......
import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
export default {
props: {
helpPagePath: {
type: String,
required: true,
},
},
template: `
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesEmptyStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>Build with confidence</h4>
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</div>
</div>
</div>
`,
};
import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
export default {
template: `
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesErrorStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
</div>
</div>
</div>
`,
};
export default {
props: {
newPipelinePath: {
type: String,
required: true,
},
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
ciLintPath: {
type: String,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
},
template: `
<div class="nav-controls">
<a
v-if="canCreatePipeline"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a>
<a
:href="ciLintPath"
class="btn btn-default">
CI Lint
</a>
</div>
`,
};
export default {
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
template: `
<ul class="nav-links">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li class="js-pipelines-tab-pending"
:class="{ 'active': scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
{{count.pending}}
</span>
</a>
</li>
<li class="js-pipelines-tab-running"
:class="{ 'active': scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
{{count.running}}
</span>
</a>
</li>
<li class="js-pipelines-tab-finished"
:class="{ 'active': scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
{{count.finished}}
</span>
</a>
</li>
<li class="js-pipelines-tab-branches"
:class="{ 'active': scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li class="js-pipelines-tab-tags"
:class="{ 'active': scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
`,
};
...@@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines'; ...@@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({ $(() => new Vue({
el: document.querySelector('.vue-pipelines-index'), el: document.querySelector('#pipelines-list-vue'),
data() { data() {
const project = document.querySelector('.pipelines');
const store = new PipelinesStore(); const store = new PipelinesStore();
return { return {
store, store,
endpoint: project.dataset.url,
}; };
}, },
components: { components: {
'vue-pipelines': PipelinesComponent, 'vue-pipelines': PipelinesComponent,
}, },
template: ` template: `
<vue-pipelines <vue-pipelines :store="store" />
:endpoint="endpoint"
:store="store" />
`, `,
})); }));
/* global Flash */
/* eslint-disable no-new */
import '~/flash';
import Vue from 'vue'; import Vue from 'vue';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination'; import TablePaginationComponent from '../vue_shared/components/table_pagination';
import EmptyState from './components/empty_state';
import ErrorState from './components/error_state';
import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls';
export default { export default {
props: { props: {
endpoint: {
type: String,
required: true,
},
store: { store: {
type: Object, type: Object,
required: true, required: true,
...@@ -23,17 +19,109 @@ export default { ...@@ -23,17 +19,109 @@ export default {
components: { components: {
'gl-pagination': TablePaginationComponent, 'gl-pagination': TablePaginationComponent,
'pipelines-table-component': PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
'empty-state': EmptyState,
'error-state': ErrorState,
'navigation-tabs': NavigationTabs,
'navigation-controls': NavigationControls,
}, },
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return { return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state, state: this.store.state,
apiScope: 'all', apiScope: 'all',
pagenum: 1, pagenum: 1,
pageRequest: false, isLoading: false,
hasError: false,
}; };
}, },
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
},
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
...@@ -69,7 +157,7 @@ export default { ...@@ -69,7 +157,7 @@ export default {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
const scope = gl.utils.getParameterByName('scope') || this.apiScope; const scope = gl.utils.getParameterByName('scope') || this.apiScope;
this.pageRequest = true; this.isLoading = true;
return this.service.getPipelines(scope, pageNumber) return this.service.getPipelines(scope, pageNumber)
.then(resp => ({ .then(resp => ({
headers: resp.headers, headers: resp.headers,
...@@ -81,41 +169,72 @@ export default { ...@@ -81,41 +169,72 @@ export default {
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
}) })
.then(() => { .then(() => {
this.pageRequest = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
this.pageRequest = false; this.hasError = true;
new Flash('An error occurred while fetching the pipelines, please reload the page again.'); this.isLoading = false;
}); });
}, },
}, },
template: `
<div>
<div class="pipelines realtime-loading" v-if="pageRequest">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon" template: `
v-if="!pageRequest && state.pipelines.length === 0"> <div :class="cssClass">
<h2 class="blank-state-title js-blank-state-title">
No pipelines to show <div
</h2> class="top-area"
v-if="!isLoading && !shouldRenderEmptyState">
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths" />
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " />
</div> </div>
<div class="table-holder" v-if="!pageRequest && state.pipelines.length"> <div class="content-list pipelines">
<pipelines-table-component
:pipelines="state.pipelines" <div
:service="service"/> class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
</div>
<gl-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"/>
</div> </div>
<gl-pagination
v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"
>
</gl-pagination>
</div> </div>
`, `,
}; };
...@@ -211,3 +211,11 @@ label { ...@@ -211,3 +211,11 @@ label {
color: $gl-text-color; color: $gl-text-color;
} }
} }
@media(max-width: $screen-xs-max) {
.remember-me {
.remember-me-checkbox {
margin-top: 0;
}
}
}
...@@ -26,7 +26,7 @@ header { ...@@ -26,7 +26,7 @@ header {
padding: 0 16px; padding: 0 16px;
z-index: 100; z-index: 100;
margin-bottom: 0; margin-bottom: 0;
height: $header-height; min-height: $header-height;
background-color: $gray-light; background-color: $gray-light;
border: none; border: none;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -85,7 +85,7 @@ header { ...@@ -85,7 +85,7 @@ header {
.navbar-toggle { .navbar-toggle {
color: $nav-toggle-gray; color: $nav-toggle-gray;
margin: 6px 0; margin: 7px 0;
border-radius: 0; border-radius: 0;
position: absolute; position: absolute;
right: -10px; right: -10px;
...@@ -135,12 +135,14 @@ header { ...@@ -135,12 +135,14 @@ header {
} }
.header-content { .header-content {
display: flex;
justify-content: space-between;
position: relative; position: relative;
height: $header-height; min-height: $header-height;
padding-left: 30px; padding-left: 30px;
@media (min-width: $screen-sm-min) { @media (max-width: $screen-sm-max) {
padding-right: 0; padding-right: 20px;
} }
.dropdown-menu { .dropdown-menu {
...@@ -165,8 +167,7 @@ header { ...@@ -165,8 +167,7 @@ header {
} }
.group-name-toggle { .group-name-toggle {
margin: 0 5px; margin: 3px 5px;
vertical-align: sub;
} }
.group-title { .group-title {
...@@ -177,39 +178,32 @@ header { ...@@ -177,39 +178,32 @@ header {
} }
} }
.title-container {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
padding-top: (($header-height - 19) / 2);
overflow: hidden;
}
.title { .title {
position: relative; position: relative;
padding-right: 20px; padding-right: 20px;
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
max-width: 385px; line-height: 22px;
display: inline-block; display: inline-block;
line-height: $header-height;
font-weight: normal; font-weight: normal;
color: $gl-text-color; color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
&.initializing { &.wrap {
display: none; white-space: normal;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
} }
@media (max-width: $screen-xs-max) { &.initializing {
max-width: 190px; opacity: 0;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
max-width: 428px;
}
@media (min-width: $screen-lg-min) {
max-width: 685px;
} }
a { a {
...@@ -226,10 +220,10 @@ header { ...@@ -226,10 +220,10 @@ header {
border: transparent; border: transparent;
background: transparent; background: transparent;
position: absolute; position: absolute;
top: 2px;
right: 3px; right: 3px;
width: 12px; width: 12px;
line-height: 19px; line-height: 19px;
margin-top: (($header-height - 19) / 2);
padding: 0; padding: 0;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
...@@ -247,7 +241,7 @@ header { ...@@ -247,7 +241,7 @@ header {
} }
.navbar-collapse { .navbar-collapse {
float: right; flex: 0 0 auto;
border-top: none; border-top: none;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
...@@ -255,7 +249,7 @@ header { ...@@ -255,7 +249,7 @@ header {
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
float: none; flex: 1 1 auto;
} }
} }
} }
...@@ -269,14 +263,6 @@ header { ...@@ -269,14 +263,6 @@ header {
} }
} }
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.header-content .title {
width: 300px;
}
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
header .container-fluid { header .container-fluid {
font-size: 18px; font-size: 18px;
......
...@@ -416,14 +416,16 @@ ...@@ -416,14 +416,16 @@
.page-with-layout-nav { .page-with-layout-nav {
.right-sidebar { .right-sidebar {
top: ($header-height * 2) + 2; top: ($header-height + 1) * 2;
} }
.build-sidebar { &.page-with-sub-nav {
top: ($header-height * 3) + 3; .right-sidebar {
top: ($header-height + 1) * 3;
&.affix { &.affix {
top: 0; top: 0;
}
} }
} }
} }
......
...@@ -176,6 +176,10 @@ summary { ...@@ -176,6 +176,10 @@ summary {
&.panel-without-border { &.panel-without-border {
border: 0; border: 0;
} }
&.panel-without-margin {
margin: 0;
}
} }
.panel-succes .panel-heading, .panel-succes .panel-heading,
......
...@@ -366,9 +366,3 @@ ...@@ -366,9 +366,3 @@
right: 0; right: 0;
margin-top: -17px; margin-top: -17px;
} }
@media (min-width: $screen-md-min) {
.sub-nav.build {
width: calc(100% + #{$gutter_width});
}
}
...@@ -142,7 +142,9 @@ ...@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
border-radius: $border-radius-default; border-radius: $border-radius-default;
margin-left: 5px; margin-left: 5px;
line-height: 1; font-size: $gl-font-size;
line-height: $gl-font-size;
outline: none;
&:hover { &:hover {
background-color: darken($gray-light, 10%); background-color: darken($gray-light, 10%);
......
...@@ -18,7 +18,10 @@ ...@@ -18,7 +18,10 @@
.environments-container { .environments-container {
.table-holder { .table-holder {
width: 100%; width: 100%;
overflow: auto;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
} }
.table.ci-table { .table.ci-table {
......
...@@ -9,6 +9,13 @@ ...@@ -9,6 +9,13 @@
} }
} }
.group-root-path {
max-width: 40vw;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: nowrap;
}
.content-list .group-name { .content-list .group-name {
font-weight: 600; font-weight: 600;
color: $pages-group-name-color; color: $pages-group-name-color;
......
...@@ -243,22 +243,6 @@ ul.notes { ...@@ -243,22 +243,6 @@ ul.notes {
} }
} }
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.note-header {
.note-headline-light {
display: block;
}
.note-actions {
position: absolute;
right: 0;
top: 0;
}
}
}
}
// Diff code in discussion view // Diff code in discussion view
.discussion-body .diff-file { .discussion-body .diff-file {
.file-title { .file-title {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.realtime-loading { .realtime-loading {
font-size: 40px; font-size: 40px;
text-align: center; text-align: center;
margin: 0 auto;
} }
.stage { .stage {
...@@ -13,9 +14,16 @@ ...@@ -13,9 +14,16 @@
white-space: nowrap; white-space: nowrap;
} }
.empty-state {
margin: 5% auto 0;
}
.table-holder { .table-holder {
width: 100%; width: 100%;
overflow: auto;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
} }
.commit-title { .commit-title {
...@@ -99,8 +107,6 @@ ...@@ -99,8 +107,6 @@
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
.content-list { .content-list {
&.pipelines,
&.environments-container,
&.builds-content-list { &.builds-content-list {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
......
...@@ -477,20 +477,6 @@ a.deploy-project-label { ...@@ -477,20 +477,6 @@ a.deploy-project-label {
} }
} }
.page-sidebar-pinned {
.project-stats .nav > li.right {
@media (min-width: $screen-lg-min) {
float: none;
}
}
.download-button {
@media (min-width: $screen-lg-min) {
margin-left: 0;
}
}
}
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
...@@ -582,54 +568,55 @@ pre.light-well { ...@@ -582,54 +568,55 @@ pre.light-well {
/* /*
* Projects list rendered on dashboard and user page * Projects list rendered on dashboard and user page
*/ */
.projects-list { .projects-list {
@include basic-list; @include basic-list;
display: flex;
flex-direction: column;
.project-row { .project-row {
border-color: $white-normal; display: flex;
align-items: center;
.project-full-name { }
@include str-truncated;
@media (max-width: $screen-xs-max) { h3 {
max-width: 50%; font-size: $gl-font-size;
} }
}
.controls { a {
line-height: $list-text-height; color: $gl-text-color;
}
.badge { .avatar-container,
@media (max-width: $screen-xs-max) { .controls {
display: none; flex: 0 0 auto;
} }
}
a:hover { .avatar-container {
text-decoration: none; align-self: flex-start;
} }
> span { .project-details {
margin-left: 10px; min-width: 0;
}
svg { p,
position: relative; .commit-row-message {
top: 2px; @include str-truncated(100%);
} margin-bottom: 0;
} }
}
.description p { .controls {
@media (max-width: $screen-xs-max) { margin-left: auto;
max-width: 50%;
}
}
} }
.bottom { .ci-status-link {
padding-top: $gl-padding; display: inline-block;
padding-bottom: 0; line-height: 17px;
vertical-align: middle;
&:hover {
text-decoration: none;
}
} }
} }
......
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
.todo-avatar, .todo-avatar,
.todo-actions { .todo-actions {
@include transition(opacity);
-webkit-flex: 0 0 auto; -webkit-flex: 0 0 auto;
flex: 0 0 auto; flex: 0 0 auto;
} }
...@@ -67,21 +68,34 @@ ...@@ -67,21 +68,34 @@
flex: 0 1 100%; flex: 0 1 100%;
min-width: 0; min-width: 0;
} }
}
.todos-list > .todo.todo-pending.done-reversible { &.todo-pending.done-reversible {
background-color: $gray-light; background-color: $white-light;
&:hover { &:hover {
border-color: $border-color; border-color: $white-dark;
} background-color: $gray-light;
.title { .todo-avatar,
font-weight: normal; .todo-item {
opacity: .6;
}
}
.todo-avatar,
.todo-item {
opacity: .2;
}
.btn {
background-color: $gray-light;
}
} }
} }
.todo-item { .todo-item {
@include transition(opacity);
.todo-title { .todo-title {
display: flex; display: flex;
......
...@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController
def create def create
opts = { opts = {
force_random_password: true, reset_password: true,
password_expires_at: nil skip_confirmation: true
} }
@user = User.new(user_params.merge(opts)) @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute
@user.created_by_id = current_user.id
@user.generate_password
@user.generate_reset_token
@user.skip_confirmation!
respond_to do |format| respond_to do |format|
if @user.save if @user.persisted?
format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' } format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user } format.json { render json: @user, status: :created, location: @user }
else else
......
...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:notification_email) params.require(:user).permit(:notification_email, :notified_of_own_activity)
end end
end end
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw] before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project' layout 'project'
def index def index
...@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def status def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) render json: BuildSerializer
.new(project: @project, user: @current_user)
.represent_status(@build)
end end
def erase def erase
......
...@@ -35,8 +35,12 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -35,8 +35,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def disable def disable
deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
return render_404 unless deploy_key_project
deploy_key_project.destroy!
load_key load_key
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
log_audit_event(@key.title, action: :destroy) log_audit_event(@key.title, action: :destroy)
redirect_to_repository_settings(@project) redirect_to_repository_settings(@project)
......
...@@ -273,4 +273,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -273,4 +273,13 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
) )
end end
def authenticate_user!
return if current_user
notice = "Please sign in to create the new issue."
store_location_for :user, request.fullpath
redirect_to new_user_session_path, notice: notice
end
end end
...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues,
# EE # EE
:approve, :approvals, :unapprove, :rebase :approve, :approvals, :unapprove, :rebase
] ]
...@@ -100,31 +100,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -100,31 +100,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs def diffs
apply_diff_view_cookie! apply_diff_view_cookie!
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
respond_to do |format| respond_to do |format|
format.html { define_discussion_vars } format.html { define_discussion_vars }
format.json do format.json do
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
if @start_sha if @start_sha
compared_diff_version compared_diff_version
else else
...@@ -428,7 +428,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -428,7 +428,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @repository.commit(@ref) @commit = @repository.commit("refs/heads/#{@ref}")
end end
render layout: false render layout: false
...@@ -439,7 +439,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -439,7 +439,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @target_project.commit(@ref) @commit = @target_project.commit("refs/heads/#{@ref}")
end end
render layout: false render layout: false
...@@ -499,6 +499,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -499,6 +499,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response render json: response
end end
def pipeline_status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
def ci_environments_status def ci_environments_status
environments = environments =
begin begin
......
...@@ -13,11 +13,14 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -13,11 +13,14 @@ class Projects::MilestonesController < Projects::ApplicationController
def index def index
@milestones = @milestones =
case params[:state] case params[:state]
when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc) when 'all' then @project.milestones
when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc) when 'closed' then @project.milestones.closed
else @project.milestones.active.reorder(due_date: :asc, title: :asc) else @project.milestones.active
end end
@sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort)
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
end end
def status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@pipeline)
end
def stage def stage
@stage = pipeline.stage(params[:stage]) @stage = pipeline.stage(params[:stage])
return not_found unless @stage return not_found unless @stage
......
...@@ -130,6 +130,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -130,6 +130,6 @@ class Projects::WikisController < Projects::ApplicationController
end end
def wiki_params def wiki_params
params[:wiki].slice(:title, :content, :format, :message) params.require(:wiki).permit(:title, :content, :format, :message)
end end
end end
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
include Recaptcha::Verify include Recaptcha::Verify
def new def new
...@@ -21,6 +20,8 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -21,6 +20,8 @@ class RegistrationsController < Devise::RegistrationsController
flash.delete :recaptcha_error flash.delete :recaptcha_error
render action: 'new' render action: 'new'
end end
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
end end
def destroy def destroy
...@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController
private private
def signup_enabled?
unless current_application_settings.signup_enabled?
redirect_to(new_user_session_path)
end
end
def sign_up_params def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end end
...@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end end
def resource def resource
@resource ||= User.new(sign_up_params) @resource ||= Users::CreateService.new(current_user, sign_up_params).build
end end
def devise_mapping def devise_mapping
......
...@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder ...@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project? if project?
if project if project
label_ids << project.group.labels if project.group.present? if project.group.present?
label_ids << project.labels labels_table = Label.arel_table
label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
)
)
else
label_ids << project.labels
end
end end
else else
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
......
...@@ -89,10 +89,12 @@ module MilestonesHelper ...@@ -89,10 +89,12 @@ module MilestonesHelper
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ") content.slice!("about ")
content << " remaining" content << " remaining"
content.html_safe
elsif milestone.start_date && milestone.start_date.past? elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days days = milestone.elapsed_days
content = content_tag(:strong, days) content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed" content << " #{'day'.pluralize(days)} elapsed"
content.html_safe
end end
end end
......
...@@ -6,7 +6,13 @@ module NamespacesHelper ...@@ -6,7 +6,13 @@ module NamespacesHelper
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups groups = current_user.owned_groups + current_user.masters_groups
groups << extra_group if extra_group && !Group.exists?(name: extra_group.name) unless extra_group.nil? || extra_group.is_a?(Group)
extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
end
if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group))
groups |= [extra_group]
end
users = [current_user.namespace] users = [current_user.namespace]
......
...@@ -31,7 +31,11 @@ module NavHelper ...@@ -31,7 +31,11 @@ module NavHelper
end end
def layout_nav_class def layout_nav_class
"page-with-layout-nav" if defined?(nav) && nav class_name = ''
class_name << " page-with-layout-nav" if defined?(nav) && nav
class_name << " page-with-sub-nav" if content_for?(:sub_nav)
class_name
end end
def nav_control_class def nav_control_class
......
...@@ -2,6 +2,7 @@ module SortingHelper ...@@ -2,6 +2,7 @@ module SortingHelper
def sort_options_hash def sort_options_hash
{ {
sort_value_name => sort_title_name, sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_updated => sort_title_recently_updated, sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated, sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created, sort_value_recently_created => sort_title_recently_created,
...@@ -52,6 +53,17 @@ module SortingHelper ...@@ -52,6 +53,17 @@ module SortingHelper
} }
end end
def milestone_sort_options_hash
{
sort_value_name => sort_title_name_asc,
sort_value_name_desc => sort_title_name_desc,
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_start_date_soon => sort_title_start_date_soon,
sort_value_start_date_later => sort_title_start_date_later,
}
end
def sort_title_priority def sort_title_priority
'Priority' 'Priority'
end end
...@@ -92,6 +104,14 @@ module SortingHelper ...@@ -92,6 +104,14 @@ module SortingHelper
'Due later' 'Due later'
end end
def sort_title_start_date_soon
'Start soon'
end
def sort_title_start_date_later
'Start later'
end
def sort_title_name def sort_title_name
'Name' 'Name'
end end
...@@ -212,6 +232,14 @@ module SortingHelper ...@@ -212,6 +232,14 @@ module SortingHelper
'due_date_desc' 'due_date_desc'
end end
def sort_value_start_date_soon
'start_date_asc'
end
def sort_value_start_date_later
'start_date_desc'
end
def sort_value_name def sort_value_name
'name_asc' 'name_asc'
end end
......
module UsersHelper
def user_link(user)
link_to(user.name, user_path(user),
title: user.email,
class: 'has-tooltip commit-committer-link')
end
end
...@@ -6,8 +6,8 @@ class Board < ActiveRecord::Base ...@@ -6,8 +6,8 @@ class Board < ActiveRecord::Base
validates :name, :project, presence: true validates :name, :project, presence: true
def done_list def closed_list
lists.merge(List.done).take lists.merge(List.closed).take
end end
def milestone def milestone
......
...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board belongs_to :board
belongs_to :label belongs_to :label
enum list_type: { label: 1, done: 2 } enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label? validates :label, :position, presence: true, if: :label?
......
...@@ -161,8 +161,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -161,8 +161,10 @@ class MergeRequest < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def self.in_projects(relation) def self.in_projects(relation)
source = where(source_project_id: relation).select(:id) # unscoping unnecessary conditions that'll be applied
target = where(target_project_id: relation).select(:id) # when executing `where("merge_requests.id IN (#{union.to_sql})")`
source = unscoped.where(source_project_id: relation).select(:id)
target = unscoped.where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target]) union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})") where("merge_requests.id IN (#{union.to_sql})")
......
...@@ -109,6 +109,21 @@ class Milestone < ActiveRecord::Base ...@@ -109,6 +109,21 @@ class Milestone < ActiveRecord::Base
end end
end end
def self.sort(method)
case method.to_s
when 'due_date_asc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
when 'due_date_desc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
when 'start_date_asc'
reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC'))
when 'start_date_desc'
reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC'))
else
order_by(method)
end
end
## ##
# Returns the String necessary to reference this Milestone in Markdown # Returns the String necessary to reference this Milestone in Markdown
# #
......
...@@ -356,20 +356,15 @@ class Project < ActiveRecord::Base ...@@ -356,20 +356,15 @@ class Project < ActiveRecord::Base
ntable = Namespace.arel_table ntable = Namespace.arel_table
pattern = "%#{query}%" pattern = "%#{query}%"
projects = select(:id).where( # unscoping unnecessary conditions that'll be applied
# when executing `where("projects.id IN (#{union.to_sql})")`
projects = unscoped.select(:id).where(
ptable[:path].matches(pattern). ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)). or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern)) or(ptable[:description].matches(pattern))
) )
# We explicitly remove any eager loading clauses as they're: namespaces = unscoped.select(:id).
#
# 1. Not needed by this query
# 2. Combined with .joins(:namespace) lead to all columns from the
# projects & namespaces tables being selected, leading to a SQL error
# due to the columns of all UNION'd queries no longer being the same.
namespaces = select(:id).
except(:includes).
joins(:namespace). joins(:namespace).
where(ntable[:name].matches(pattern)) where(ntable[:name].matches(pattern))
......
...@@ -1024,7 +1024,7 @@ class Repository ...@@ -1024,7 +1024,7 @@ class Repository
end end
def diverged_from_upstream?(branch_name) def diverged_from_upstream?(branch_name)
branch_commit = commit(branch_name) branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}") upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit if upstream_commit
...@@ -1035,7 +1035,7 @@ class Repository ...@@ -1035,7 +1035,7 @@ class Repository
end end
def upstream_has_diverged?(branch_name, remote_ref) def upstream_has_diverged?(branch_name, remote_ref)
branch_commit = commit(branch_name) branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{remote_ref}/#{branch_name}") upstream_commit = commit("refs/remotes/#{remote_ref}/#{branch_name}")
if upstream_commit if upstream_commit
...@@ -1046,7 +1046,7 @@ class Repository ...@@ -1046,7 +1046,7 @@ class Repository
end end
def up_to_date_with_upstream?(branch_name) def up_to_date_with_upstream?(branch_name)
branch_commit = commit(branch_name) branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}") upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit if upstream_commit
......
...@@ -24,6 +24,7 @@ class User < ActiveRecord::Base ...@@ -24,6 +24,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false default_value_for :hide_no_password, false
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
...@@ -126,7 +127,9 @@ class User < ActiveRecord::Base ...@@ -126,7 +127,9 @@ class User < ActiveRecord::Base
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, validates :username,
namespace: true, namespace: true,
presence: true, presence: true,
...@@ -137,10 +140,9 @@ class User < ActiveRecord::Base ...@@ -137,10 +140,9 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
...@@ -150,8 +152,6 @@ class User < ActiveRecord::Base ...@@ -150,8 +152,6 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights before_save :ensure_external_user_rights
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
after_initialize :set_projects_limit after_initialize :set_projects_limit
before_create :check_confirmation_email
after_create :post_create_hook
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
# User's Layout preference # User's Layout preference
...@@ -409,10 +409,8 @@ class User < ActiveRecord::Base ...@@ -409,10 +409,8 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}" "#{self.class.reference_prefix}#{username}"
end end
def generate_password def skip_confirmation=(bool)
if force_random_password skip_confirmation! if bool
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end end
def generate_reset_token def generate_reset_token
...@@ -424,10 +422,6 @@ class User < ActiveRecord::Base ...@@ -424,10 +422,6 @@ class User < ActiveRecord::Base
@reset_token @reset_token
end end
def check_confirmation_email
skip_confirmation! unless current_application_settings.send_user_confirmation_email
end
def recently_sent_password_reset? def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end end
...@@ -822,12 +816,6 @@ class User < ActiveRecord::Base ...@@ -822,12 +816,6 @@ class User < ActiveRecord::Base
end end
end end
def post_create_hook
log_info("User \"#{name}\" (#{email}) was created")
notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed") log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy) system_hook_service.execute_hooks_for(self, :destroy)
......
...@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity ...@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
private private
alias_method :build, :object
def path_to(route, build) def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) send("#{route}_path", build.project.namespace, build.project, build)
end end
def detailed_status
build.detailed_status(request.user)
end
end end
class BuildSerializer < BaseSerializer
entity BuildEntity
def represent_status(resource)
data = represent(resource, { only: [:status] })
data.fetch(:status, {})
end
end
...@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity ...@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end end
expose :details do expose :details do
expose :status do |pipeline, options| expose :detailed_status, as: :status, with: StatusEntity
StatusEntity.represent(
pipeline.detailed_status(request.user),
options)
end
expose :duration expose :duration
expose :finished_at expose :finished_at
expose :stages, using: StageEntity expose :stages, using: StageEntity
...@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity ...@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? && pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline) can?(request.user, :update_pipeline, pipeline)
end end
def detailed_status
pipeline.detailed_status(request.user)
end
end end
...@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer ...@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts) super(resource, opts)
end end
end end
def represent_status(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
end
end end
class StatusEntity < Grape::Entity class StatusEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :icon, :text, :label, :group expose :icon, :favicon, :text, :label, :group
expose :has_details?, as: :has_details expose :has_details?, as: :has_details
expose :details_path expose :details_path
......
...@@ -4,7 +4,7 @@ module Boards ...@@ -4,7 +4,7 @@ module Boards
board = project.boards.create(params) board = project.boards.create(params)
if board.persisted? if board.persisted?
board.lists.create(list_type: :done) board.lists.create(list_type: :closed)
end end
board board
......
...@@ -41,7 +41,7 @@ module Boards ...@@ -41,7 +41,7 @@ module Boards
end end
def set_state def set_state
params[:state] = list && list.done? ? 'closed' : 'opened' params[:state] = list && list.closed? ? 'closed' : 'opened'
end end
def board_label_ids def board_label_ids
......
...@@ -48,8 +48,8 @@ module Boards ...@@ -48,8 +48,8 @@ module Boards
end end
def issue_state def issue_state
return 'reopen' if moving_from_list.done? return 'reopen' if moving_from_list.closed?
return 'close' if moving_to_list.done? return 'close' if moving_to_list.closed?
end end
def add_label_ids def add_label_ids
......
...@@ -7,14 +7,14 @@ module Ci ...@@ -7,14 +7,14 @@ module Ci
raise Gitlab::Access::AccessDeniedError raise Gitlab::Access::AccessDeniedError
end end
pipeline.builds.failed_or_canceled.find_each do |build| pipeline.builds.latest.failed_or_canceled.find_each do |build|
next unless build.retryable? next unless build.retryable?
Ci::RetryBuildService.new(project, current_user) Ci::RetryBuildService.new(project, current_user)
.reprocess(build) .reprocess(build)
end end
pipeline.builds.skipped.find_each do |skipped| pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process } retry_optimistic_lock(skipped) { |build| build.process }
end end
......
...@@ -38,7 +38,7 @@ class NotificationRecipientService ...@@ -38,7 +38,7 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target) recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) if skip_current_user recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
recipients.uniq recipients.uniq
end end
...@@ -47,7 +47,7 @@ class NotificationRecipientService ...@@ -47,7 +47,7 @@ class NotificationRecipientService
recipients = add_labels_subscribers([], target, labels: labels) recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target) recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) recipients.delete(current_user) unless current_user.notified_of_own_activity?
recipients.uniq recipients.uniq
end end
...@@ -88,7 +88,7 @@ class NotificationRecipientService ...@@ -88,7 +88,7 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable) recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author) recipients.delete(note.author) unless note.author.notified_of_own_activity?
recipients.uniq recipients.uniq
end end
......
...@@ -297,8 +297,9 @@ class NotificationService ...@@ -297,8 +297,9 @@ class NotificationService
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline, pipeline,
nil, # The acting user, who won't be added to recipients pipeline.user,
action: pipeline.status).map(&:notification_email) action: pipeline.status,
skip_current_user: false).map(&:notification_email)
if recipients.any? if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later mailer.public_send(email_template, pipeline, recipients).deliver_later
......
module Users
# Service for creating a new user.
class CreateService < BaseService
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
end
def build
raise Gitlab::Access::AccessDeniedError unless can_create_user?
user = User.new(build_user_params)
if current_user&.is_admin?
if params[:reset_password]
@reset_token = user.generate_reset_token
params[:force_random_password] = true
end
if params[:force_random_password]
random_password = Devise.friendly_token.first(Devise.password_length.min)
user.password = user.password_confirmation = random_password
end
end
identity_attrs = params.slice(:extern_uid, :provider)
if identity_attrs.any?
user.identities.build(identity_attrs)
end
user
end
def execute
user = build
if user.save
log_info("User \"#{user.name}\" (#{user.email}) was created")
notification_service.new_user(user, @reset_token) if @reset_token
system_hook_service.execute_hooks_for(user, :create)
end
user
end
private
def can_create_user?
(current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin?
end
# Allowed params for creating a user (admins only)
def admin_create_params
[
:access_level,
:admin,
:avatar,
:bio,
:can_create_group,
:color_scheme_id,
:email,
:external,
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
:linkedin,
:name,
:password,
:password_expires_at,
:projects_limit,
:remember_me,
:skip_confirmation,
:skype,
:theme_id,
:twitter,
:username,
:website_url
]
end
# Allowed params for user signup
def signup_params
[
:email,
:email_confirmation,
:name,
:password,
:username
]
end
def build_user_params
if current_user&.is_admin?
user_params = params.slice(*admin_create_params)
user_params[:created_by_id] = current_user.id
if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil)
end
else
user_params = params.slice(*signup_params)
user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
end
user_params
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%legend Access %legend Access
.form-group .form-group
= f.label :projects_limit, class: 'control-label' = f.label :projects_limit, class: 'control-label'
.col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group .form-group
= f.label :can_create_group, class: 'control-label' = f.label :can_create_group, class: 'control-label'
......
...@@ -68,12 +68,11 @@ ...@@ -68,12 +68,11 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do = link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created = sort_title_oldest_created
.js-todos-all .js-todos-all
- if @todos.any? - if @todos.any?
.js-todos-list-container .js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
.panel.panel-default.panel-small.panel-without-border .panel.panel-default.panel-without-border.panel-without-margin
%ul.content-list.todos-list %ul.content-list.todos-list
= render @todos = render @todos
= paginate @todos, theme: "gitlab" = paginate @todos, theme: "gitlab"
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
%label{ for: "user_remember_me" } %label{ for: "user_remember_me" }
= f.check_box :remember_me = f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me %span Remember me
.pull-right.forgot-password .pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name) = link_to "Forgot your password?", new_password_path(resource_name)
......
...@@ -17,24 +17,3 @@ ...@@ -17,24 +17,3 @@
= link_to filter_projects_path(visibility_level: level) do = link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level) = visibility_level_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
- if @tags.present?
.dropdown
%button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('tags')
%span.light Tags:
- if params[:tag].present?
= params[:tag]
- else
Any
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
Any
- @tags.each do |tag|
%li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
.layout-nav .layout-nav
.container-fluid .container-fluid
= render "layouts/nav/#{nav}" = render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" } - if content_for?(:sub_nav)
= yield :sub_nav = yield :sub_nav
.content-wrapper{ class: layout_nav_class }
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/flash" = render "layouts/flash"
......
...@@ -15,6 +15,13 @@ ...@@ -15,6 +15,13 @@
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('ellipsis-v') = icon('ellipsis-v')
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
.title-container
%h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
...@@ -69,12 +76,6 @@ ...@@ -69,12 +76,6 @@
%div %div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
%h1.title{ class: ('initializing' if @has_group_title) }= title
= yield :header_content = yield :header_content
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
......
...@@ -34,6 +34,11 @@ ...@@ -34,6 +34,11 @@
.clearfix .clearfix
= form_for @user, url: profile_notifications_path, method: :put do |f|
%label{ for: 'user_notified_of_own_activity' }
= f.check_box :notified_of_own_activity
%span Receive notifications about your own activity
%hr %hr
%h5 %h5
Groups (#{@group_notifications.count}) Groups (#{@group_notifications.count})
......
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
data: { container: "body", placement: "bottom" } } data: { container: "body", placement: "bottom" } }
{{ list.title }} {{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' } %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }} {{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm", "@click" => "showNewIssueForm",
"v-if" => 'list.type !== "done"', "v-if" => 'list.type !== "closed"',
"aria-label" => "Add an issue", "aria-label" => "Add an issue",
"title" => "Add an issue", "title" => "Add an issue",
data: { placement: "top", container: "body" } } data: { placement: "top", container: "body" } }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= icon("spinner spin") = icon("spinner spin")
- if can? current_user, :create_issue, @project - if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list", %board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "done" && showIssueForm' } "v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list", %ul.board-list{ "ref" => "list",
"v-show" => "!loading", "v-show" => "!loading",
":data-board" => "list.id", ":data-board" => "list.id",
......
- builds = @build.pipeline.builds.to_a - builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job Job
%strong ##{@build.id} %strong ##{@build.id}
...@@ -137,3 +137,6 @@ ...@@ -137,3 +137,6 @@
= build.id = build.id
- if build.retried? - if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
:javascript
new Sidebar();
- @no_container = true - @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs" - page_title "#{@build.name} (##{@build.id})", "Jobs"
= render "projects/pipelines/head", build_subnav: true = render "projects/pipelines/head"
%div{ class: container_class } %div{ class: container_class }
.build-page .build-page
......
- disable_initialization = local_assigns.fetch(:disable_initialization, false) - disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint, endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
} } } }
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.col-sm-6 .col-sm-6
.nav-controls .nav-controls
= link_to @environment.external_url, class: 'btn btn-default' do = link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('external-link') = icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment = render 'projects/deployments/actions', deployment: @environment.last_deployment
......
- case @status - case @status
- when :success - when :success
- remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch?
:plain :plain
merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); merge_request_widget.mergeInProgress(#{remove_source_branch});
- when :merge_when_pipeline_succeeds - when :merge_when_pipeline_succeeds
:plain :plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}"); $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls .nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone New Milestone
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
.input-group-addon .input-group-addon
= root_url = root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else - else
.input-group-addon.static-namespace .input-group-addon.static-namespace
......
= content_for :sub_nav do = content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll .scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll' = render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) } %ul{ class: (container_class) }
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(path: 'pipelines#index', controller: :pipelines) do = nav_link(path: 'pipelines#index', controller: :pipelines) do
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
Pipelines Pipelines
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(path: 'builds#index', controller: :builds) do = nav_link(controller: :builds) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span %span
Jobs Jobs
- if project_nav_tab? :environments - if project_nav_tab? :environments
= nav_link(path: 'environments#index', controller: :environments) do = nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span %span
Environments Environments
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment