Merge branch 'master' into sm-cherry-pick-list-commits-in-message

This commit is contained in:
Saverio Miroddi 2017-08-15 18:49:28 +02:00
commit cd80a9075f
461 changed files with 6728 additions and 2671 deletions

View File

@ -27,6 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
before_script: before_script:
- bundle --version - bundle --version
@ -45,16 +46,17 @@ stages:
tags: tags:
- gitlab-org - gitlab-org
.knapsack-state: &knapsack-state .tests-metadata-state: &tests-metadata-state
services: [] services: []
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache" TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/
.use-pg: &use-pg .use-pg: &use-pg
services: services:
@ -86,7 +88,7 @@ stages:
except: except:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/
.rspec-knapsack: &rspec-knapsack .rspec-metadata: &rspec-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
stage: test stage: test
@ -96,8 +98,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]} - export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_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
- export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
@ -106,20 +113,21 @@ stages:
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- rspec_flaky/
- tmp/capybara/ - tmp/capybara/
.rspec-knapsack-pg: &rspec-knapsack-pg .rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-knapsack <<: *rspec-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs <<: *except-docs
.rspec-knapsack-mysql: &rspec-knapsack-mysql .rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-knapsack <<: *rspec-metadata
<<: *use-mysql <<: *use-mysql
<<: *only-if-want-mysql <<: *only-if-want-mysql
<<: *except-docs <<: *except-docs
.spinach-knapsack: &spinach-knapsack .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
stage: test stage: test
@ -140,13 +148,13 @@ stages:
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
.spinach-knapsack-pg: &spinach-knapsack-pg .spinach-metadata-pg: &spinach-metadata-pg
<<: *spinach-knapsack <<: *spinach-metadata
<<: *use-pg <<: *use-pg
<<: *except-docs <<: *except-docs
.spinach-knapsack-mysql: &spinach-knapsack-mysql .spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-knapsack <<: *spinach-metadata
<<: *use-mysql <<: *use-mysql
<<: *only-if-want-mysql <<: *only-if-want-mysql
<<: *except-docs <<: *except-docs
@ -176,40 +184,71 @@ build-package:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
# Prepare and merge knapsack tests # Retrieve knapsack and rspec_flaky reports
knapsack: retrieve-tests-metadata:
<<: *knapsack-state <<: *tests-metadata-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
stage: prepare stage: prepare
cache: cache:
key: knapsack key: tests_metadata
paths:
- knapsack/
policy: pull policy: pull
script: script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
- 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 - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- 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 - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_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_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
update-knapsack: update-tests-metadata:
<<: *knapsack-state <<: *tests-metadata-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *only-canonical-masters <<: *only-canonical-masters
stage: post-test stage: post-test
cache: cache:
key: knapsack key: tests_metadata
paths: paths:
- knapsack/ - knapsack/
- rspec_flaky/
policy: push policy: push
script: script:
- retry gem install fog-aws mime-types - retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
flaky-examples-check:
<<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine
services: []
before_script: []
cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
stage: post-test
allow_failure: yes
only:
- branches
except:
- master
artifacts:
expire_in: 30d
paths:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env: setup-test-env:
<<: *use-pg <<: *use-pg
@ -232,69 +271,69 @@ setup-test-env:
- public/assets - public/assets
- tmp/tests - tmp/tests
rspec-pg 0 25: *rspec-knapsack-pg rspec-pg 0 25: *rspec-metadata-pg
rspec-pg 1 25: *rspec-knapsack-pg rspec-pg 1 25: *rspec-metadata-pg
rspec-pg 2 25: *rspec-knapsack-pg rspec-pg 2 25: *rspec-metadata-pg
rspec-pg 3 25: *rspec-knapsack-pg rspec-pg 3 25: *rspec-metadata-pg
rspec-pg 4 25: *rspec-knapsack-pg rspec-pg 4 25: *rspec-metadata-pg
rspec-pg 5 25: *rspec-knapsack-pg rspec-pg 5 25: *rspec-metadata-pg
rspec-pg 6 25: *rspec-knapsack-pg rspec-pg 6 25: *rspec-metadata-pg
rspec-pg 7 25: *rspec-knapsack-pg rspec-pg 7 25: *rspec-metadata-pg
rspec-pg 8 25: *rspec-knapsack-pg rspec-pg 8 25: *rspec-metadata-pg
rspec-pg 9 25: *rspec-knapsack-pg rspec-pg 9 25: *rspec-metadata-pg
rspec-pg 10 25: *rspec-knapsack-pg rspec-pg 10 25: *rspec-metadata-pg
rspec-pg 11 25: *rspec-knapsack-pg rspec-pg 11 25: *rspec-metadata-pg
rspec-pg 12 25: *rspec-knapsack-pg rspec-pg 12 25: *rspec-metadata-pg
rspec-pg 13 25: *rspec-knapsack-pg rspec-pg 13 25: *rspec-metadata-pg
rspec-pg 14 25: *rspec-knapsack-pg rspec-pg 14 25: *rspec-metadata-pg
rspec-pg 15 25: *rspec-knapsack-pg rspec-pg 15 25: *rspec-metadata-pg
rspec-pg 16 25: *rspec-knapsack-pg rspec-pg 16 25: *rspec-metadata-pg
rspec-pg 17 25: *rspec-knapsack-pg rspec-pg 17 25: *rspec-metadata-pg
rspec-pg 18 25: *rspec-knapsack-pg rspec-pg 18 25: *rspec-metadata-pg
rspec-pg 19 25: *rspec-knapsack-pg rspec-pg 19 25: *rspec-metadata-pg
rspec-pg 20 25: *rspec-knapsack-pg rspec-pg 20 25: *rspec-metadata-pg
rspec-pg 21 25: *rspec-knapsack-pg rspec-pg 21 25: *rspec-metadata-pg
rspec-pg 22 25: *rspec-knapsack-pg rspec-pg 22 25: *rspec-metadata-pg
rspec-pg 23 25: *rspec-knapsack-pg rspec-pg 23 25: *rspec-metadata-pg
rspec-pg 24 25: *rspec-knapsack-pg rspec-pg 24 25: *rspec-metadata-pg
rspec-mysql 0 25: *rspec-knapsack-mysql rspec-mysql 0 25: *rspec-metadata-mysql
rspec-mysql 1 25: *rspec-knapsack-mysql rspec-mysql 1 25: *rspec-metadata-mysql
rspec-mysql 2 25: *rspec-knapsack-mysql rspec-mysql 2 25: *rspec-metadata-mysql
rspec-mysql 3 25: *rspec-knapsack-mysql rspec-mysql 3 25: *rspec-metadata-mysql
rspec-mysql 4 25: *rspec-knapsack-mysql rspec-mysql 4 25: *rspec-metadata-mysql
rspec-mysql 5 25: *rspec-knapsack-mysql rspec-mysql 5 25: *rspec-metadata-mysql
rspec-mysql 6 25: *rspec-knapsack-mysql rspec-mysql 6 25: *rspec-metadata-mysql
rspec-mysql 7 25: *rspec-knapsack-mysql rspec-mysql 7 25: *rspec-metadata-mysql
rspec-mysql 8 25: *rspec-knapsack-mysql rspec-mysql 8 25: *rspec-metadata-mysql
rspec-mysql 9 25: *rspec-knapsack-mysql rspec-mysql 9 25: *rspec-metadata-mysql
rspec-mysql 10 25: *rspec-knapsack-mysql rspec-mysql 10 25: *rspec-metadata-mysql
rspec-mysql 11 25: *rspec-knapsack-mysql rspec-mysql 11 25: *rspec-metadata-mysql
rspec-mysql 12 25: *rspec-knapsack-mysql rspec-mysql 12 25: *rspec-metadata-mysql
rspec-mysql 13 25: *rspec-knapsack-mysql rspec-mysql 13 25: *rspec-metadata-mysql
rspec-mysql 14 25: *rspec-knapsack-mysql rspec-mysql 14 25: *rspec-metadata-mysql
rspec-mysql 15 25: *rspec-knapsack-mysql rspec-mysql 15 25: *rspec-metadata-mysql
rspec-mysql 16 25: *rspec-knapsack-mysql rspec-mysql 16 25: *rspec-metadata-mysql
rspec-mysql 17 25: *rspec-knapsack-mysql rspec-mysql 17 25: *rspec-metadata-mysql
rspec-mysql 18 25: *rspec-knapsack-mysql rspec-mysql 18 25: *rspec-metadata-mysql
rspec-mysql 19 25: *rspec-knapsack-mysql rspec-mysql 19 25: *rspec-metadata-mysql
rspec-mysql 20 25: *rspec-knapsack-mysql rspec-mysql 20 25: *rspec-metadata-mysql
rspec-mysql 21 25: *rspec-knapsack-mysql rspec-mysql 21 25: *rspec-metadata-mysql
rspec-mysql 22 25: *rspec-knapsack-mysql rspec-mysql 22 25: *rspec-metadata-mysql
rspec-mysql 23 25: *rspec-knapsack-mysql rspec-mysql 23 25: *rspec-metadata-mysql
rspec-mysql 24 25: *rspec-knapsack-mysql rspec-mysql 24 25: *rspec-metadata-mysql
spinach-pg 0 5: *spinach-knapsack-pg spinach-pg 0 5: *spinach-metadata-pg
spinach-pg 1 5: *spinach-knapsack-pg spinach-pg 1 5: *spinach-metadata-pg
spinach-pg 2 5: *spinach-knapsack-pg spinach-pg 2 5: *spinach-metadata-pg
spinach-pg 3 5: *spinach-knapsack-pg spinach-pg 3 5: *spinach-metadata-pg
spinach-pg 4 5: *spinach-knapsack-pg spinach-pg 4 5: *spinach-metadata-pg
spinach-mysql 0 5: *spinach-knapsack-mysql spinach-mysql 0 5: *spinach-metadata-mysql
spinach-mysql 1 5: *spinach-knapsack-mysql spinach-mysql 1 5: *spinach-metadata-mysql
spinach-mysql 2 5: *spinach-knapsack-mysql spinach-mysql 2 5: *spinach-metadata-mysql
spinach-mysql 3 5: *spinach-knapsack-mysql spinach-mysql 3 5: *spinach-metadata-mysql
spinach-mysql 4 5: *spinach-knapsack-mysql spinach-mysql 4 5: *spinach-metadata-mysql
# Static analysis jobs # Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis .ruby-static-analysis: &ruby-static-analysis
@ -354,7 +393,7 @@ ee_compat_check:
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?/
allow_failure: yes allow_failure: yes
cache: cache:
key: "ee_compat_check_repo" key: "ee_compat_check_repo"

View File

@ -1045,7 +1045,7 @@ RSpec/BeforeAfterAll:
RSpec/DescribeClass: RSpec/DescribeClass:
Enabled: false Enabled: false
# Use `described_class` for tested class / module. # Checks that the second argument to `describe` specifies a method.
RSpec/DescribeMethod: RSpec/DescribeMethod:
Enabled: false Enabled: false
@ -1053,8 +1053,7 @@ RSpec/DescribeMethod:
RSpec/DescribeSymbol: RSpec/DescribeSymbol:
Enabled: true Enabled: true
# Checks that the second argument to top level describe is the tested method # Checks that tests use `described_class`.
# name.
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: true Enabled: true
@ -1099,6 +1098,11 @@ RSpec/FilePath:
RSpec/Focus: RSpec/Focus:
Enabled: true Enabled: true
# Checks the arguments passed to `before`, `around`, and `after`.
RSpec/HookArgument:
Enabled: true
EnforcedStyle: implicit
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: is_expected, should # SupportedStyles: is_expected, should
RSpec/ImplicitExpect: RSpec/ImplicitExpect:

View File

@ -70,12 +70,6 @@ RSpec/EmptyLineAfterFinalLet:
RSpec/EmptyLineAfterSubject: RSpec/EmptyLineAfterSubject:
Enabled: false Enabled: false
# Offense count: 78
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
RSpec/HookArgument:
Enabled: false
# Offense count: 9 # Offense count: 9
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: it_behaves_like, it_should_behave_like # SupportedStyles: it_behaves_like, it_should_behave_like

View File

@ -2,6 +2,32 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.4.5 (2017-08-14)
- Fix deletion of deploy keys linked to other projects. !13162
- Allow any logged in users to read_users_list even if it's restricted. !13201
- Make Delete Merged Branches handle wildcard protected branches correctly. !13251
- Fix an order of operations for CI connection error message in merge request widget. !13252
- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286
- Add missing validation error for username change with container registry tags. !13356
- Fix destroy of case-insensitive conflicting redirects. !13357
- Project pending delete no longer return 500 error in admins projects view. !13389
- Fix search box losing focus when typing.
- Use jQuery to control scroll behavior in job log for cross browser consistency.
- Use project_ref_path to create the link to a branch to fix links that 404.
- improve file upload/replace experience.
- fix jump to next discussion button.
- Fixes new issue button for failed job returning 404.
- Fix links to group milestones from issue and merge request sidebar.
- Fixed sign-in restrictions buttons not toggling active state.
- Fix Mattermost integration.
- Change project FK migration to skip existing FKs.
## 9.4.4 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 9.4.3 (2017-07-31) ## 9.4.3 (2017-07-31)
- Fix Prometheus client PID reuse bug. !13130 - Fix Prometheus client PID reuse bug. !13130
@ -226,6 +252,11 @@ entry.
- Log rescued exceptions to Sentry. - Log rescued exceptions to Sentry.
- Remove remaining N+1 queries in merge requests API with emojis and labels. - Remove remaining N+1 queries in merge requests API with emojis and labels.
## 9.3.10 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 9.3.9 (2017-07-20) ## 9.3.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions. - Fix an infinite loop when handling user-supplied regular expressions.
@ -498,6 +529,11 @@ entry.
- Remove foreigh key on ci_trigger_schedules only if it exists. - Remove foreigh key on ci_trigger_schedules only if it exists.
- Allow translation of Pipeline Schedules. - Allow translation of Pipeline Schedules.
## 9.2.10 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 9.2.9 (2017-07-20) ## 9.2.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions. - Fix an infinite loop when handling user-supplied regular expressions.
@ -753,6 +789,11 @@ entry.
- Fix preemptive scroll bar on user activity calendar. - Fix preemptive scroll bar on user activity calendar.
- Pipeline chat notifications convert seconds to minutes and hours. - Pipeline chat notifications convert seconds to minutes and hours.
## 9.1.10 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 9.1.9 (2017-07-20) ## 9.1.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions. - Fix an infinite loop when handling user-supplied regular expressions.
@ -1076,6 +1117,11 @@ entry.
- Only send chat notifications for the default branch. - Only send chat notifications for the default branch.
- Don't fill in the default kubernetes namespace. - Don't fill in the default kubernetes namespace.
## 9.0.13 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 9.0.12 (2017-07-20) ## 9.0.12 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions. - Fix an infinite loop when handling user-supplied regular expressions.
@ -1456,6 +1502,11 @@ entry.
- Change development tanuki favicon colors to match logo color order. - Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids. - API issues - support filtering by iids.
## 8.17.8 (2017-08-09)
- Remove hidden symlinks from project import files.
- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
## 8.17.7 (2017-07-19) ## 8.17.7 (2017-07-19)
- Renders 404 if given project is not readable by the user on Todos dashboard. - Renders 404 if given project is not readable by the user on Todos dashboard.

View File

@ -1 +1 @@
0.29.0 0.30.0

View File

@ -64,7 +64,7 @@ gem 'gpgme'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap' gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap' gem 'net-ldap'
# Git Wiki # Git Wiki
@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 1.0'
# HAML # HAML
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
@ -324,6 +324,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
@ -402,7 +403,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.26.0' gem 'gitaly', '~> 0.27.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false

View File

@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2) ace-rails-ap (4.1.2)
actionmailer (4.2.8) actionmailer (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0) acts-as-taggable-on (4.0.0)
activerecord (>= 4.0) activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
@ -124,6 +128,9 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
colorize (0.7.7) colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5) concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5) concurrent-ruby (= 1.0.5)
@ -270,7 +277,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.26.0) gitaly (0.27.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
@ -289,7 +296,7 @@ GEM
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.5.1) gitlab-markup (1.5.1)
gitlab_omniauth-ldap (2.0.3) gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16) net-ldap (~> 0.16)
omniauth (~> 1.3) omniauth (~> 1.3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1) pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
@ -419,9 +426,18 @@ GEM
json-schema (2.6.2) json-schema (2.6.2)
addressable (~> 2.3.8) addressable (~> 2.3.8)
jwt (1.5.6) jwt (1.5.6)
kaminari (0.17.0) kaminari (1.0.1)
actionpack (>= 3.0.0) activesupport (>= 4.1.0)
activesupport (>= 3.0.0) kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0) knapsack (1.11.0)
rake rake
@ -461,6 +477,8 @@ GEM
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
@ -601,6 +619,11 @@ GEM
premailer-rails (1.9.7) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta11) prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
@ -709,6 +732,10 @@ GEM
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2) rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0) rspec-core (3.6.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-expectations (3.6.0) rspec-expectations (3.6.0)
@ -717,6 +744,12 @@ GEM
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0) rspec-rails (3.6.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
@ -883,6 +916,14 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
unparser (0.2.6)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
procto (~> 0.0.2)
url_safe_base64 (0.2.2) url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
@ -984,11 +1025,11 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.26.0) gitaly (~> 0.27.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 2.0.3) gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
@ -1011,7 +1052,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 0.17.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.11.0)
kubeclient (~> 2.2.0) kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
@ -1085,6 +1126,7 @@ DEPENDENCIES
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)

View File

@ -1,7 +1,7 @@
/* global ListIssue */ /* global ListIssue */
/* global bp */
import Vue from 'vue'; import Vue from 'vue';
import bp from '../../../breakpoints';
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;

View File

@ -1,66 +1,19 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */ export const breakpoints = {
lg: 1200,
md: 992,
sm: 768,
xs: 0,
};
var Breakpoints = (function() { const BreakpointInstance = {
var BreakpointInstance, instance; windowWidth: () => window.innerWidth,
getBreakpointSize() {
const windowWidth = this.windowWidth();
function Breakpoints() {} const breakpoint = Object.keys(breakpoints).find(key => windowWidth > breakpoints[key]);
instance = null; return breakpoint;
},
};
BreakpointInstance = (function() { export default BreakpointInstance;
var BREAKPOINTS;
BREAKPOINTS = ["xs", "sm", "md", "lg"];
function BreakpointInstance() {
this.setup();
}
BreakpointInstance.prototype.setup = function() {
var allDeviceSelector, els;
allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
return ".device-" + breakpoint;
});
if ($(allDeviceSelector.join(",")).length) {
return;
}
// Create all the elements
els = $.map(BREAKPOINTS, function(breakpoint) {
return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
});
return $("body").append(els.join(''));
};
BreakpointInstance.prototype.visibleDevice = function() {
var allDeviceSelector;
allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
return ".device-" + breakpoint;
});
return $(allDeviceSelector.join(",")).filter(":visible");
};
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
// TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
}
$visibleDevice = this.visibleDevice();
return $visibleDevice.attr("class").split("visible-")[1];
};
return BreakpointInstance;
})();
Breakpoints.get = function() {
return instance != null ? instance : instance = new BreakpointInstance;
};
return Breakpoints;
})();
$(() => { window.bp = Breakpoints.get(); });
window.Breakpoints = Breakpoints;

View File

@ -1,8 +1,7 @@
/* eslint-disable func-names, wrap-iife, no-use-before-define, /* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */ consistent-return, prefer-rest-params */
/* global Breakpoints */
import _ from 'underscore'; import _ from 'underscore';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils'; import { bytesToKiB } from './lib/utils/number_utils';
window.Build = (function () { window.Build = (function () {
@ -34,8 +33,6 @@ window.Build = (function () {
this.$scrollBottomBtn = $('.js-scroll-down'); this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(Build.timeout); clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar(); this.initSidebar();
this.populateJobs(this.buildStage); this.populateJobs(this.buildStage);
@ -230,7 +227,7 @@ window.Build = (function () {
}; };
Build.prototype.shouldHideSidebarForViewport = function () { Build.prototype.shouldHideSidebarForViewport = function () {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}; };

View File

@ -347,6 +347,9 @@ import initChangesDropdown from './init_changes_dropdown';
if ($('#tree-slider').length) new TreeView(); if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer(); if ($('.blob-viewer').length) new BlobViewer();
if ($('.project-show-activity').length) new gl.Activities(); if ($('.project-show-activity').length) new gl.Activities();
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
break; break;
case 'projects:edit': case 'projects:edit':
setupProjectEdit(); setupProjectEdit();

View File

@ -1,6 +1,19 @@
/* global bp */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import './breakpoints'; import bp from './breakpoints';
const HIDE_INTERVAL_TIMEOUT = 300;
const IS_OVER_CLASS = 'is-over';
const IS_ABOVE_CLASS = 'is-above';
const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
let currentOpenMenu = null;
let menuCornerLocs;
let timeoutId;
export const mousePos = [];
export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
export const canShowActiveSubItems = (el) => { export const canShowActiveSubItems = (el) => {
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
@ -11,8 +24,28 @@ export const canShowActiveSubItems = (el) => {
return true; return true;
}; };
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0;
const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0];
const currentMousePosY = currentMousePos.y;
const [menuTop, menuBottom] = menuCornerLocs;
if (currentMousePosY < menuTop.y ||
currentMousePosY > menuBottom.y) return 0;
if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) {
return HIDE_INTERVAL_TIMEOUT;
}
return 0;
};
export const calculateTop = (boundingRect, outerHeight) => { export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
@ -21,45 +54,118 @@ export const calculateTop = (boundingRect, outerHeight) => {
boundingRect.top; boundingRect.top;
}; };
export const showSubLevelItems = (el) => { export const hideMenu = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items'); if (!el) return;
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; const parentEl = el.parentNode;
subItems.style.display = 'block'; el.style.display = ''; // eslint-disable-line no-param-reassign
el.classList.add('is-showing-fly-out'); el.style.transform = ''; // eslint-disable-line no-param-reassign
el.classList.add('is-over'); el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
setOpenMenu();
};
export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect(); const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight); const top = calculateTop(boundingRect, subItems.offsetHeight);
const isAbove = top < boundingRect.top; const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list'); subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect();
menuCornerLocs = [
{
x: subItemsRect.left, // left position of the sub items
y: subItemsRect.top, // top position of the sub items
},
{
x: subItemsRect.left, // left position of the sub items
y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
},
];
if (isAbove) { if (isAbove) {
subItems.classList.add('is-above'); subItems.classList.add(IS_ABOVE_CLASS);
} }
}; };
export const hideSubLevelItems = (el) => { export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items'); const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
el.classList.remove('is-showing-fly-out'); el.classList.add(IS_OVER_CLASS);
el.classList.remove('is-over');
subItems.style.display = ''; if (!subItems) return;
subItems.style.transform = '';
subItems.classList.remove('is-above'); subItems.style.display = 'block';
el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
setOpenMenu(subItems);
moveSubItemsToPosition(el, subItems);
};
export const mouseEnterTopItems = (el) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
showSubLevelItems(el);
}, getHideSubItemsInterval());
};
export const mouseLeaveTopItem = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!canShowSubItems() || !canShowActiveSubItems(el) ||
(subItems && subItems === currentOpenMenu)) return;
el.classList.remove(IS_OVER_CLASS);
};
export const documentMouseMove = (e) => {
mousePos.push({
x: e.clientX,
y: e.clientY,
});
if (mousePos.length > 6) mousePos.shift();
}; };
export default () => { export default () => {
const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] const sidebar = document.querySelector('.sidebar-top-level-items');
.filter(el => el.querySelector('.sidebar-sub-level-items'));
if (!sidebar) return;
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
items.forEach((el) => { items.forEach((el) => {
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); const subItems = el.querySelector('.sidebar-sub-level-items');
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
if (subItems) {
subItems.addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
hideMenu(currentOpenMenu);
}); });
}
el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget));
el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget));
});
document.addEventListener('mousemove', documentMouseMove);
}; };

View File

@ -1,12 +1,14 @@
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({ $.get({
url: form.data('signatures-path'), url: form.data('signatures-path'),
data: form.serialize(), data: form.serialize(),
}).done((response) => { }).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => { response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
}); });

View File

@ -1,7 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global bp */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7; const PARTICIPANTS_ROW_COUNT = 7;

View File

@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global bp */
/* global Flash */ /* global Flash */
/* global ConfirmDangerModal */ /* global ConfirmDangerModal */
/* global Aside */ /* global Aside */
@ -66,7 +65,7 @@ import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './breakpoints'; import bp from './breakpoints';
import './broadcast_message'; import './broadcast_message';
import './build'; import './build';
import './build_artifacts'; import './build_artifacts';

View File

@ -1,13 +1,12 @@
/* eslint-disable no-new, class-methods-use-this */ /* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Flash */ /* global Flash */
/* global notes */ /* global notes */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import './breakpoints';
import './flash'; import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
@ -134,7 +133,7 @@ import initChangesDropdown from './init_changes_dropdown';
this.destroyPipelinesView(); this.destroyPipelinesView();
} else if (this.isDiffAction(action)) { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if (Breakpoints.get().getBreakpointSize() !== 'lg') { if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
@ -145,7 +144,7 @@ import initChangesDropdown from './init_changes_dropdown';
this.resetViewContainer(); this.resetViewContainer();
this.mountPipelinesView(); this.mountPipelinesView();
} else { } else {
if (Breakpoints.get().getBreakpointSize() !== 'xs') { if (bp.getBreakpointSize() !== 'xs') {
this.expandView(); this.expandView();
} }
this.resetViewContainer(); this.resetViewContainer();
@ -392,7 +391,7 @@ import initChangesDropdown from './init_changes_dropdown';
// Screen space on small screens is usually very sparse // Screen space on small screens is usually very sparse
// So we dont affix the tabs on these // So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
/** /**
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.

View File

@ -1,5 +1,4 @@
<script> <script>
/* global Breakpoints */
import d3 from 'd3'; import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue'; import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue'; import monitoringFlag from './monitoring_flag.vue';
@ -8,6 +7,7 @@
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../lib/utils/number_utils';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
@ -42,7 +42,6 @@
yScale: {}, yScale: {},
margin: {}, margin: {},
data: [], data: [],
breakpointHandler: Breakpoints.get(),
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8', areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1', lineColorRgb: '#1f78d1',
@ -96,7 +95,7 @@
methods: { methods: {
draw() { draw() {
const breakpointSize = this.breakpointHandler.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.columnData.queries[0]; const query = this.columnData.queries[0];
this.margin = measurements.large.margin; this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') { if (breakpointSize === 'xs' || breakpointSize === 'sm') {

View File

@ -1,7 +1,6 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
/* global bp */ import bp from './breakpoints';
import './breakpoints';
export default class NewNavSidebar { export default class NewNavSidebar {
constructor() { constructor() {

View File

@ -1,50 +0,0 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
`,
};

View File

@ -0,0 +1,59 @@
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
created() {
this.illustrationSvg = illustrationSvg;
},
};
</script>
<template>
<div
v-if="!calloutDismissed"
class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i
aria-hidden="true"
class="fa fa-times">
</i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
</template>

View File

@ -1,5 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout', el: '#pipeline-schedules-callout',

View File

@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`; return `${this.job.name} - ${this.job.status.label}`;
}, },
}, },
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
mounted() {
this.stopDropdownClickPropagation();
},
}; };
</script> </script>
<template> <template>

View File

@ -36,7 +36,7 @@ const bindEvents = () => {
$('.how_to_import_link').on('click', (e) => { $('.how_to_import_link').on('click', (e) => {
e.preventDefault(); e.preventDefault();
$('.how_to_import_link').next('.modal').show(); $(e.currentTarget).next('.modal').show();
}); });
$('.modal-header .close').on('click', () => { $('.modal-header .close').on('click', () => {

View File

@ -29,12 +29,10 @@ export default {
editMode() { editMode() {
if (this.editMode) { if (this.editMode) {
$('.project-refs-form').addClass('disabled'); $('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show(); $('.js-tree-ref-target-holder').show();
$('.project-refs-target-form').show();
} else { } else {
$('.project-refs-form').removeClass('disabled'); $('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide(); $('.js-tree-ref-target-holder').hide();
$('.project-refs-target-form').hide();
} }
}, },
}, },

View File

@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() { mounted() {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}, },
computed: { computed: {
html() { html() {
@ -12,10 +12,16 @@ export default {
}, },
}, },
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}); });
}, },
}, },
@ -24,9 +30,23 @@ export default {
<template> <template>
<div> <div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div> <div
<div v-if="activeFile.render_error" class="vertical-center render-error"> v-if="!activeFile.render_error"
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -33,32 +33,30 @@ const RepoSidebar = {
}); });
}, },
linkClicked(clickedFile) { fileClicked(clickedFile) {
let url = '';
let file = clickedFile; let file = clickedFile;
if (typeof file === 'object') {
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
url = file.url; Service.url = file.url;
Service.url = url; Helper.getContent(file)
// I need to refactor this to do the `then` here. .then(() => {
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
}); })
} .catch(Helper.loadingError);
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
} }
}, },
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
@ -82,7 +80,7 @@ export default RepoSidebar;
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="isRoot"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/> @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file <repo-loading-file
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
@ -94,7 +92,7 @@ export default RepoSidebar;
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini" :is-mini="isMini"
@linkclicked="linkClicked(file)" @linkclicked="fileClicked(file)"
:is-tree="isTree" :is-tree="isTree"
:has-files="!!files.length" :has-files="!!files.length"
:active-file="activeFile"/> :active-file="activeFile"/>

View File

@ -10,6 +10,12 @@ const RepoTab = {
}, },
computed: { computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times': !this.tab.changed, 'fa-times': !this.tab.changed,
@ -34,12 +40,24 @@ export default RepoTab;
<template> <template>
<li> <li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> <a
<i class="fa" :class="changedClass"></i> href="#0"
class="close"
@click.prevent="xClicked(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a> </a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> <a
href="#"
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li> </li>
</template> </template>

View File

@ -1,5 +1,4 @@
<script> <script>
import Vue from 'vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
@ -14,29 +13,19 @@ const RepoTabs = {
data: () => Store, data: () => Store,
methods: { methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) { xClicked(file) {
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
}, },
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
}; };
export default RepoTabs; export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> <ul
v-if="isMini"
id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </ul>

View File

@ -10,7 +10,10 @@ function repoEditorLoader() {
Store.monaco = monaco; Store.monaco = monaco;
Store.monacoLoading = false; Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, reject); }, () => {
Store.monacoLoading = false;
reject();
});
}); });
} }

View File

@ -33,12 +33,16 @@ const RepoHelper = {
? window.performance ? window.performance
: Date, : Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getBranch() { getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref'); return $('button.dropdown-menu-toggle').attr('data-ref');
}, },
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop(); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext'; return foundLang ? foundLang.id : 'plaintext';
@ -135,21 +139,19 @@ const RepoHelper = {
return isRoot; return isRoot;
}, },
getContent(treeOrFile, cb) { getContent(treeOrFile) {
let file = treeOrFile; let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true); // const loadingData = RepoHelper.setLoading(true);
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
// RepoHelper.setLoading(false, loadingData); // RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
if (data.binary) { if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
@ -188,9 +190,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url; newFile.url = file.url;
if (newFile.render_error === 'too_large') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = '';
@ -199,10 +200,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) { serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message; simpleBlob.lastCommitMessage = blob.last_commit.message;
@ -226,7 +223,7 @@ const RepoHelper = {
type, type,
name, name,
url, url,
icon: RepoHelper.toFA(icon), icon: `fa-${icon}`,
level: 0, level: 0,
loading: false, loading: false,
}; };
@ -244,7 +241,7 @@ const RepoHelper = {
setTimeout(() => { setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = 12000; tabs.scrollLeft = tabs.scrollWidth;
}, 200); }, 200);
}, },

View File

@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
$('.project-refs-target-form').hide(); $('.js-tree-ref-target-holder').hide();
$('.fa-long-arrow-right').hide();
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {

View File

@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = { const RepoService = {
url: '', url: '',
@ -22,6 +23,7 @@ const RepoService = {
getRaw(url) { getRaw(url) {
return axios.get(url, { return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
}, },
@ -36,7 +38,7 @@ const RepoService = {
}, },
urlIsRichBlob(url = this.url) { urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop(); const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension); return this.richExtensionRegExp.test(extension);
}, },

View File

@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
ideEl: {},
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {}, monacoInstance: {},
service: '', service: '',
editor: '',
sidebar: '',
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
@ -17,19 +14,10 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '', blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false, submitCommitsLoading: false,
binaryLoaded: false,
dialog: { dialog: {
open: false, open: false,
title: '', title: '',
@ -45,9 +33,6 @@ const RepoStore = {
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch', targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: { binaryTypes: {
png: false, png: false,
md: false, md: false,
@ -58,7 +43,6 @@ const RepoStore = {
tree: false, tree: false,
blob: false, blob: false,
}, },
readOnly: true,
resetBinaryTypes() { resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => { Object.keys(RepoStore.binaryTypes).forEach((key) => {
@ -96,7 +80,6 @@ const RepoStore = {
if (file.binary) { if (file.binary) {
RepoStore.blobRaw = file.base64; RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
@ -238,4 +221,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview'; return RepoStore.currentBlobView === 'repo-preview';
}, },
}; };
export default RepoStore; export default RepoStore;

View File

@ -71,7 +71,7 @@ export default {
/> />
<div v-if="!isConfidential" class="no-value confidential-value"> <div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i> <i class="fa fa-eye is-not-confidential"></i>
None This issue is not confidential
</div> </div>
<div v-else class="value confidential-value hide-collapsed"> <div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>

View File

@ -1,10 +1,7 @@
/* global Breakpoints */ import bp from './breakpoints';
import './breakpoints';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false; this.sidebarExpanded = false;
@ -41,15 +38,15 @@ export default class Wikis {
this.renderSidebar(); this.renderSidebar();
} }
sidebarCanCollapse() { static sidebarCanCollapse() {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
} }
renderSidebar() { renderSidebar() {
if (!this.sidebarEl) return; if (!this.sidebarEl) return;
const { classList } = this.sidebarEl; const { classList } = this.sidebarEl;
if (this.sidebarExpanded || !this.sidebarCanCollapse()) { if (this.sidebarExpanded || !Wikis.sidebarCanCollapse()) {
if (!classList.contains('right-sidebar-expanded')) { if (!classList.contains('right-sidebar-expanded')) {
classList.remove('right-sidebar-collapsed'); classList.remove('right-sidebar-collapsed');
classList.add('right-sidebar-expanded'); classList.add('right-sidebar-expanded');

View File

@ -725,9 +725,9 @@
} }
// TODO: change global style and remove mixin // TODO: change global style and remove mixin
@mixin new-style-dropdown { @mixin new-style-dropdown($selector: '') {
.dropdown-menu, #{$selector}.dropdown-menu,
.dropdown-menu-nav { #{$selector}.dropdown-menu-nav {
.divider { .divider {
margin: 6px 0; margin: 6px 0;
} }
@ -773,7 +773,7 @@
} }
} }
.dropdown-menu-align-right { #{$selector}.dropdown-menu-align-right {
margin-top: 2px; margin-top: 2px;
} }
} }

View File

@ -339,6 +339,8 @@ a > code {
@extend .ref-name; @extend .ref-name;
} }
@include new-style-dropdown('.git-revision-dropdown');
/** /**
* Apply Markdown typography * Apply Markdown typography
* *

View File

@ -403,6 +403,7 @@ header.navbar-gitlab-new {
} }
.breadcrumbs-extra { .breadcrumbs-extra {
display: flex;
flex: 0 0 auto; flex: 0 0 auto;
margin-left: auto; margin-left: auto;
} }

View File

@ -103,6 +103,7 @@ $new-sidebar-collapsed-width: 50px;
&.sidebar-icons-only { &.sidebar-icons-only {
width: $new-sidebar-collapsed-width; width: $new-sidebar-collapsed-width;
overflow-x: hidden;
.badge, .badge,
.project-title { .project-title {
@ -249,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
position: absolute; position: absolute;
top: -30px; top: -30px;
bottom: -30px; bottom: -30px;
left: 0; left: -10px;
right: -30px; right: -30px;
z-index: -1; z-index: -1;
} }
&::after {
content: "";
position: absolute;
top: 44px;
left: -30px;
right: 35px;
bottom: 0;
height: 100%;
max-height: 150px;
z-index: -1;
transform: skew(33deg);
}
&.is-above { &.is-above {
margin-top: 1px; margin-top: 1px;
&::after {
top: auto;
bottom: 44px;
transform: skew(-30deg);
}
} }
> .active { > .active {
@ -321,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
} }
} }
&:not(.active):hover > a, &.active > a:hover,
> a:hover,
&.is-over > a { &.is-over > a {
background-color: $white-light; background-color: $white-light;
} }

View File

@ -286,6 +286,10 @@
.gpg-status-box { .gpg-status-box {
&:empty {
display: none;
}
&.valid { &.valid {
@include green-status-color; @include green-status-color;
} }

View File

@ -108,6 +108,7 @@
background-color: $orange-50; background-color: $orange-50;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
border-bottom: none;
padding: 3px 12px; padding: 3px 12px;
margin: auto; margin: auto;
align-items: center; align-items: center;
@ -132,22 +133,9 @@
} }
} }
.not-confidential { .confidential-issue-warning + .md-area {
padding: 0; border-top-left-radius: 0;
border-top: none; border-top-right-radius: 0;
}
.right-sidebar-expanded {
.md-area {
border-radius: 0;
border-top: none;
}
}
.right-sidebar-collapsed {
.confidential-issue-warning {
border-bottom: none;
}
} }
.discussion-form { .discussion-form {

View File

@ -453,7 +453,10 @@ ul.notes {
} }
.note-actions { .note-actions {
align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
display: inline-flex;
align-items: center;
// For PhantomJS that does not support flex // For PhantomJS that does not support flex
float: right; float: right;
margin-left: 10px; margin-left: 10px;
@ -463,18 +466,12 @@ ul.notes {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
} }
.more-actions { .more-actions {
display: inline-block; float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
@ -482,16 +479,10 @@ ul.notes {
} }
.more-actions-toggle { .more-actions-toggle {
padding: 0;
&:hover .icon, &:hover .icon,
&:focus .icon { &:focus .icon {
color: $blue-600; color: $blue-600;
} }
.icon {
padding: 0 6px;
}
} }
.more-actions-dropdown { .more-actions-dropdown {
@ -519,28 +510,42 @@ ul.notes {
@include notes-media('max', $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
.note-action-button {
margin-left: 0;
} }
}
.note-actions-item {
margin-left: 15px;
display: flex;
align-items: center;
&.more-actions {
// compensate for narrow icon
margin-left: 10px;
} }
} }
.note-action-button { .note-action-button {
display: inline; line-height: 1;
line-height: 20px; padding: 0;
min-width: 16px;
color: $gray-darkest;
.fa { .fa {
color: $gray-darkest;
position: relative; position: relative;
font-size: 17px; font-size: 16px;
} }
svg { svg {
height: 16px; height: 16px;
width: 16px; width: 16px;
fill: $gray-darkest; top: 0;
vertical-align: text-top; vertical-align: text-top;
path {
fill: currentColor;
}
} }
.award-control-icon-positive, .award-control-icon-positive,
@ -613,10 +618,7 @@ ul.notes {
.note-role { .note-role {
position: relative; position: relative;
top: -2px; padding: 0 7px;
display: inline-block;
padding-left: 7px;
padding-right: 7px;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;

View File

@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle {
* Top arrow in the dropdown in the mini pipeline graph * Top arrow in the dropdown in the mini pipeline graph
*/ */
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
z-index: 200;
&::before, &::before,
&::after { &::after {

View File

@ -29,6 +29,10 @@
margin-right: 15px; margin-right: 15px;
} }
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb { .repo-breadcrumb {
li:last-of-type { li:last-of-type {
position: relative; position: relative;

View File

@ -45,7 +45,7 @@ class Admin::AppearancesController < Admin::ApplicationController
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_appearance def set_appearance
@appearance = Appearance.last || Appearance.new @appearance = Appearance.current || Appearance.new
end end
# Only allow a trusted parameter "white list" through. # Only allow a trusted parameter "white list" through.

View File

@ -69,7 +69,7 @@ module AuthenticatesWithTwoFactor
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
# Remove any lingering user data from login # Remove any lingering user data from login
session.delete(:otp_user_id) session.delete(:otp_user_id)
session.delete(:challenges) session.delete(:challenge)
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)

View File

@ -6,6 +6,13 @@ module CycleAnalyticsParams
end end
def start_date(params) def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago case params[:start_date]
when '7'
7.days.ago
when '30'
30.days.ago
else
90.days.ago
end
end end
end end

View File

@ -52,8 +52,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def load_events def load_events
@events = Event.in_projects(load_projects(params.merge(non_public: true))) projects = load_projects(params.merge(non_public: true))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
end end
end end

View File

@ -29,9 +29,9 @@ class DashboardController < Dashboard::ApplicationController
current_user.authorized_projects current_user.authorized_projects
end end
@events = Event.in_projects(projects) @events = EventCollection
@events = @event_filter.apply_filter(@events).with_associations .new(projects, offset: params[:offset].to_i, filter: @event_filter)
@events = @events.limit(20).offset(params[:offset] || 0) .to_a
end end
def set_show_full_reference def set_show_full_reference

View File

@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def index def index
params[:sort] ||= 'latest_activity_desc' params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
params[:trending] = true params[:trending] = true
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def starred def starred
@projects = load_projects.reorder('star_count DESC').page(params[:page]) @projects = load_projects.reorder('star_count DESC')
respond_to do |format| respond_to do |format|
format.html format.html
@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: params) ProjectsFinder.new(current_user: current_user, params: params)
.execute.includes(:route, namespace: :route) .execute
.includes(:route, namespace: :route)
.page(params[:page])
.without_count
end end
end end

View File

@ -160,9 +160,9 @@ class GroupsController < Groups::ApplicationController
end end
def load_events def load_events
@events = Event.in_projects(@projects) @events = EventCollection
@events = event_filter.apply_filter(@events).with_associations .new(@projects, offset: params[:offset].to_i, filter: event_filter)
@events = @events.limit(20).offset(params[:offset] || 0) .to_a
end end
def user_actions def user_actions

View File

@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request def create_merge_request
result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])

View File

@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :repository, except: [:index, :new, :create] before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
# Authorize # Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@ -301,10 +302,11 @@ class ProjectsController < Projects::ApplicationController
end end
def load_events def load_events
@events = @project.events.recent projects = Project.where(id: @project.id)
@events = event_filter.apply_filter(@events).with_associations
limit = (params[:limit] || 20).to_i @events = EventCollection
@events = @events.limit(limit).offset(params[:offset] || 0) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
end end
def project_params def project_params
@ -389,4 +391,8 @@ class ProjectsController < Projects::ApplicationController
url_for(params) url_for(params)
end end
def project_export_enabled
render_404 unless current_application_settings.project_export_enabled?
end
end end

View File

@ -1,3 +1,4 @@
# :nocov:
if Rails.env.test? if Rails.env.test?
class UnicornTestController < ActionController::Base class UnicornTestController < ActionController::Base
def pid def pid
@ -10,3 +11,4 @@ if Rails.env.test?
end end
end end
end end
# :nocov:

View File

@ -18,7 +18,7 @@ class Admin::ProjectsFinder
end end
def execute def execute
items = Project.with_statistics items = Project.without_deleted.with_statistics
items = items.in_namespace(namespace_id) if namespace_id.present? items = items.in_namespace(namespace_id) if namespace_id.present?
items = items.where(visibility_level: visibility_level) if visibility_level.present? items = items.where(visibility_level: visibility_level) if visibility_level.present?
items = items.with_push if with_push.present? items = items.with_push if with_push.present?

View File

@ -20,7 +20,7 @@ module AppearancesHelper
end end
def brand_item def brand_item
@appearance ||= Appearance.first @appearance ||= Appearance.current
end end
def brand_header_logo def brand_header_logo

View File

@ -146,6 +146,7 @@ module ApplicationSettingsHelper
:plantuml_enabled, :plantuml_enabled,
:plantuml_url, :plantuml_url,
:polling_interval_multiplier, :polling_interval_multiplier,
:project_export_enabled,
:prometheus_metrics_enabled, :prometheus_metrics_enabled,
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_private_key, :recaptcha_private_key,

View File

@ -59,7 +59,7 @@ module GroupsHelper
end end
def remove_group_message(group) def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name } { group_name: group.name }
end end

View File

@ -0,0 +1,21 @@
module PaginationHelper
def paginate_collection(collection, remote: nil)
if collection.is_a?(Kaminari::PaginatableWithoutCount)
paginate_without_count(collection)
elsif collection.respond_to?(:total_pages)
paginate_with_count(collection, remote: remote)
end
end
def paginate_without_count(collection)
render(
'kaminari/gitlab/without_count',
previous_path: path_to_prev_page(collection),
next_path: path_to_next_page(collection)
)
end
def paginate_with_count(collection, remote: nil)
paginate(collection, remote: remote, theme: 'gitlab')
end
end

View File

@ -80,7 +80,7 @@ module ProjectsHelper
end end
def remove_project_message(project) def remove_project_message(project)
_("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace } { project_name_with_namespace: project.name_with_namespace }
end end
@ -234,6 +234,8 @@ module ProjectsHelper
# If no limit is applied we'll just issue a COUNT since the result set could # If no limit is applied we'll just issue a COUNT since the result set could
# be too large to load into memory. # be too large to load into memory.
def any_projects?(projects) def any_projects?(projects)
return projects.any? if projects.is_a?(Array)
if projects.limit_value if projects.limit_value
projects.to_a.any? projects.to_a.any?
else else

View File

@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type @member_source_type = member_source_type
@member_id = member_id @member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) admins = member_source.members.owners_and_masters.pluck(:notification_email)
# A project in a group can have no explicit owners/masters, in that case # A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters. # we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group if admins.empty? && member_source.respond_to?(:group) && member_source.group
admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) admins = member_source.group.members.owners_and_masters.pluck(:notification_email)
end end
mail(to: admins, mail(to: admins,

View File

@ -8,7 +8,27 @@ class Appearance < ActiveRecord::Base
validates :logo, file_size: { maximum: 1.megabyte } validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte } validates :header_logo, file_size: { maximum: 1.megabyte }
validate :single_appearance_row, on: :create
mount_uploader :logo, AttachmentUploader mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = 'current_appearance'.freeze
after_commit :flush_redis_cache
def self.current
Rails.cache.fetch(CACHE_KEY) { first }
end
def flush_redis_cache
Rails.cache.delete(CACHE_KEY)
end
def single_appearance_row
if self.class.any?
errors.add(:single_appearance_row, 'Only 1 appearances row can exist')
end
end
end end

View File

@ -241,6 +241,7 @@ class ApplicationSetting < ActiveRecord::Base
performance_bar_allowed_group_id: nil, performance_bar_allowed_group_id: nil,
plantuml_enabled: false, plantuml_enabled: false,
plantuml_url: nil, plantuml_url: nil,
project_export_enabled: true,
recaptcha_enabled: false, recaptcha_enabled: false,
repository_checks_enabled: true, repository_checks_enabled: true,
repository_storages: ['default'], repository_storages: ['default'],

View File

@ -14,9 +14,15 @@ class BroadcastMessage < ActiveRecord::Base
default_value_for :color, '#E75E40' default_value_for :color, '#E75E40'
default_value_for :font, '#FFFFFF' default_value_for :font, '#FFFFFF'
CACHE_KEY = 'broadcast_message_current'.freeze
after_commit :flush_redis_cache
def self.current def self.current
Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do Rails.cache.fetch(CACHE_KEY) do
where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a where('ends_at > :now AND starts_at <= :now', now: Time.zone.now)
.reorder(id: :asc)
.to_a
end end
end end
@ -31,4 +37,8 @@ class BroadcastMessage < ActiveRecord::Base
def ended? def ended?
ends_at < Time.zone.now ends_at < Time.zone.now
end end
def flush_redis_cache
Rails.cache.delete(CACHE_KEY)
end
end end

View File

@ -48,6 +48,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id
# For Hash only # For Hash only
serialize :data # rubocop:disable Cop/ActiveRecordSerialize serialize :data # rubocop:disable Cop/ActiveRecordSerialize
@ -55,19 +56,51 @@ class Event < ActiveRecord::Base
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push? after_create :set_last_repository_updated_at, if: :push?
after_create :replicate_event_for_push_events_migration
# Scopes # Scopes
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, -> (projects) do
where(project_id: projects.pluck(:id)).recent sub_query = projects
.except(:order)
.select(1)
.where('projects.id = events.project_id')
where('EXISTS (?)', sub_query).recent
end
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built).
includes(:author, :project, project: :namespace)
.preload(:target, :push_event_payload)
end end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
self.inheritance_column = 'action'
class << self class << self
def find_sti_class(action)
if action.to_i == PUSHED
PushEvent
else
Event
end
end
def subclass_from_attributes(attrs)
# Without this Rails will keep calling this method on the returned class,
# resulting in an infinite loop.
return unless self == Event
action = attrs.with_indifferent_access[inheritance_column].to_i
PushEvent if action == PUSHED
end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes # Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)", where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
@ -290,6 +323,16 @@ class Event < ActiveRecord::Base
@commits ||= (data[:commits] || []).reverse @commits ||= (data[:commits] || []).reverse
end end
def commit_title
commit = commits.last
commit[:message] if commit
end
def commit_id
commit_to || commit_from
end
def commits_count def commits_count
data[:total_commits_count] || commits.count || 0 data[:total_commits_count] || commits.count || 0
end end
@ -385,6 +428,16 @@ class Event < ActiveRecord::Base
user ? author_id == user.id : false user ? author_id == user.id : false
end end
# We're manually replicating data into the new table since database triggers
# are not dumped to db/schema.rb. This could mean that a new installation
# would not have the triggers in place, thus losing events data in GitLab
# 10.0.
def replicate_event_for_push_events_migration
new_attributes = attributes.with_indifferent_access.except(:title, :data)
EventForMigration.create!(new_attributes)
end
private private
def recent_update? def recent_update?

View File

@ -0,0 +1,98 @@
# A collection of events to display in an event list.
#
# An EventCollection is meant to be used for displaying events to a user (e.g.
# in a controller), it's not suitable for building queries that are used for
# building other queries.
class EventCollection
# To prevent users from putting too much pressure on the database by cycling
# through thousands of events we put a limit on the number of pages.
MAX_PAGE = 10
# projects - An ActiveRecord::Relation object that returns the projects for
# which to retrieve events.
# filter - An EventFilter instance to use for filtering events.
def initialize(projects, limit: 20, offset: 0, filter: nil)
@projects = projects
@limit = limit
@offset = offset
@filter = filter
end
# Returns an Array containing the events.
def to_a
return [] if current_page > MAX_PAGE
relation = if Gitlab::Database.join_lateral_supported?
relation_with_join_lateral
else
relation_without_join_lateral
end
relation.with_associations.to_a
end
private
# Returns the events relation to use when JOIN LATERAL is not supported.
#
# This relation simply gets all the events for all authorized projects, then
# limits that set.
def relation_without_join_lateral
events = filtered_events.in_projects(projects)
paginate_events(events)
end
# Returns the events relation to use when JOIN LATERAL is supported.
#
# This relation is built using JOIN LATERAL, producing faster queries than a
# regular LIMIT + OFFSET approach.
def relation_with_join_lateral
projects_for_lateral = projects.select(:id).to_sql
lateral = filtered_events
.limit(limit_for_join_lateral)
.where('events.project_id = projects_for_lateral.id')
.to_sql
# The outer query does not need to re-apply the filters since the JOIN
# LATERAL body already takes care of this.
outer = base_relation
.from("(#{projects_for_lateral}) projects_for_lateral")
.joins("JOIN LATERAL (#{lateral}) AS #{Event.table_name} ON true")
paginate_events(outer)
end
def filtered_events
@filter ? @filter.apply_filter(base_relation) : base_relation
end
def paginate_events(events)
events.limit(@limit).offset(@offset)
end
def base_relation
# We want to have absolute control over the event queries being built, thus
# we're explicitly opting out of any default scopes that may be set.
Event.unscoped.recent
end
def limit_for_join_lateral
# Applying the OFFSET on the inside of a JOIN LATERAL leads to incorrect
# results. To work around this we need to increase the inner limit for every
# page.
#
# This means that on page 1 we use LIMIT 20, and an outer OFFSET of 0. On
# page 2 we use LIMIT 40 and an outer OFFSET of 20.
@limit + @offset
end
def current_page
(@offset / @limit) + 1
end
def projects
@projects.except(:order)
end
end

View File

@ -0,0 +1,5 @@
# This model is used to replicate events between the old "events" table and the
# new "events_for_migration" table that will replace "events" in GitLab 10.0.
class EventForMigration < ActiveRecord::Base
self.table_name = 'events_for_migration'
end

View File

@ -212,21 +212,39 @@ class Group < Namespace
end end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
users_with_parents.pluck(:id) members_with_parents.pluck(:user_id)
end end
def members_with_parents def members_with_parents
GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) # Avoids an unnecessary SELECT when the group has no parents
source_ids =
if parent_id
self_and_ancestors.reorder(nil).select(:id)
else
id
end
GroupMember
.active_without_invites
.where(source_id: source_ids)
end
def members_with_descendants
GroupMember
.active_without_invites
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end end
def users_with_parents def users_with_parents
User.where(id: members_with_parents.select(:user_id)) User
.where(id: members_with_parents.select(:user_id))
.reorder(nil)
end end
def users_with_descendants def users_with_descendants
members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) User
.where(id: members_with_descendants.select(:user_id))
User.where(id: members_with_descendants.select(:user_id)) .reorder(nil)
end end
def max_member_access_for_user(user) def max_member_access_for_user(user)

View File

@ -41,9 +41,20 @@ class Member < ActiveRecord::Base
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_active = User.arel_table[:state].eq(:active) user_is_active = User.arel_table[:state].eq(:active)
includes(:user).references(:users) user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
.where(is_external_invite.or(user_is_active))
left_join_users
.where(user_ok)
.where(requested_at: nil) .where(requested_at: nil)
.reorder(nil)
end
# Like active, but without invites. For when a User is required.
scope :active_without_invites, -> do
left_join_users
.where(users: { state: 'active' })
.where(requested_at: nil)
.reorder(nil)
end end
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
@ -276,6 +287,13 @@ class Member < ActiveRecord::Base
@notification_setting ||= user.notification_settings_for(source) @notification_setting ||= user.notification_settings_for(source)
end end
def notifiable?(type, opts = {})
# always notify when there isn't a user yet
return true if user.blank?
NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
end
private private
def send_invite def send_invite
@ -332,4 +350,8 @@ class Member < ActiveRecord::Base
def notification_service def notification_service
NotificationService.new NotificationService.new
end end
def notifiable_options
{}
end
end end

View File

@ -30,6 +30,10 @@ class GroupMember < Member
'Group' 'Group'
end end
def notifiable_options
{ group: group }
end
private private
def send_invite def send_invite

View File

@ -87,6 +87,10 @@ class ProjectMember < Member
project.owner == user project.owner == user
end end
def notifiable_options
{ project: project }
end
private private
def delete_member_todos def delete_member_todos

View File

@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
end end
def reload_diff_if_branch_changed def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed? if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
reload_diff reload_diff
end end
end end
@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base
end end
def fetch_ref def fetch_ref
target_project.repository.fetch_ref( write_ref
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
ref_path
)
update_column(:ref_fetched, true) update_column(:ref_fetched, true)
end end
@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base
true true
end end
private
def write_ref
target_project.repository.with_repo_branch_commit(
source_project.repository, source_branch) do |commit|
if commit
target_project.repository.write_ref(ref_path, commit.sha)
else
raise Rugged::ReferenceError, 'source repository is empty'
end
end
end
end end

View File

@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors .base_and_ancestors
end end
def self_and_ancestors
return self.class.where(id: id) unless parent_id
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
end
# Returns all the descendants of the current namespace. # Returns all the descendants of the current namespace.
def descendants def descendants
Gitlab::GroupHierarchy Gitlab::GroupHierarchy
@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants .base_and_descendants
end end
def self_and_descendants
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
[owner_id] [owner_id]
end end

View File

@ -5,14 +5,22 @@ class NotificationRecipient
custom_action: nil, custom_action: nil,
target: nil, target: nil,
acting_user: nil, acting_user: nil,
project: nil project: nil,
group: nil,
skip_read_ability: false
) )
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
@custom_action = custom_action @custom_action = custom_action
@acting_user = acting_user @acting_user = acting_user
@target = target @target = target
@project = project || @target&.project @project = project || default_project
@group = group || @project&.group
@user = user @user = user
@type = type @type = type
@skip_read_ability = skip_read_ability
end end
def notification_setting def notification_setting
@ -77,6 +85,8 @@ class NotificationRecipient
def has_access? def has_access?
DeclarativePolicy.subject_scope do DeclarativePolicy.subject_scope do
return false unless user.can?(:receive_notifications) return false unless user.can?(:receive_notifications)
return true if @skip_read_ability
return false if @project && !user.can?(:read_project, @project) return false if @project && !user.can?(:read_project, @project)
return true unless read_ability return true unless read_ability
@ -96,6 +106,7 @@ class NotificationRecipient
private private
def read_ability def read_ability
return nil if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability) return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability = @read_ability =
@ -111,12 +122,18 @@ class NotificationRecipient
end end
end end
def default_project
return nil if @target.nil?
return @target if @target.is_a?(Project)
return @target.project if @target.respond_to?(:project)
end
def find_notification_setting def find_notification_setting
project_setting = @project && user.notification_settings_for(@project) project_setting = @project && user.notification_settings_for(@project)
return project_setting unless project_setting.nil? || project_setting.global? return project_setting unless project_setting.nil? || project_setting.global?
group_setting = @project&.group && user.notification_settings_for(@project.group) group_setting = @group && user.notification_settings_for(@group)
return group_setting unless group_setting.nil? || group_setting.global? return group_setting unless group_setting.nil? || group_setting.global?

View File

@ -196,7 +196,6 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base
def change_head(branch) def change_head(branch)
if repository.branch_exists?(branch) if repository.branch_exists?(branch)
repository.before_change_head repository.before_change_head
repository.rugged.references.create('HEAD', repository.write_ref('HEAD', "refs/heads/#{branch}")
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.after_change_head repository.after_change_head
reload_default_branch reload_default_branch
@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch # @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
def forks_count
Projects::ForksCountService.new(self).count
end
private private
def cross_namespace_reference?(from) def cross_namespace_reference?(from)

126
app/models/push_event.rb Normal file
View File

@ -0,0 +1,126 @@
class PushEvent < Event
# This validation exists so we can't accidentally use PushEvent with a
# different "action" value.
validate :validate_push_action
# Authors are required as they're used to display who pushed data.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid user.
validates :author_id, presence: true
# The project is required to build links to commits, commit ranges, etc.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid project.
validates :project_id, presence: true
# The "data" field must not be set for push events since it's not used and a
# waste of space.
validates :data, absence: true
# These fields are also not used for push events, thus storing them would be a
# waste.
validates :target_id, absence: true
validates :target_type, absence: true
def self.sti_name
PUSHED
end
def push?
true
end
def push_with_commits?
!!(commit_from && commit_to)
end
def tag?
return super unless push_event_payload
push_event_payload.tag?
end
def branch?
return super unless push_event_payload
push_event_payload.branch?
end
def valid_push?
return super unless push_event_payload
push_event_payload.ref.present?
end
def new_ref?
return super unless push_event_payload
push_event_payload.created?
end
def rm_ref?
return super unless push_event_payload
push_event_payload.removed?
end
def commit_from
return super unless push_event_payload
push_event_payload.commit_from
end
def commit_to
return super unless push_event_payload
push_event_payload.commit_to
end
def ref_name
return super unless push_event_payload
push_event_payload.ref
end
def ref_type
return super unless push_event_payload
push_event_payload.ref_type
end
def branch_name
return super unless push_event_payload
ref_name
end
def tag_name
return super unless push_event_payload
ref_name
end
def commit_title
return super unless push_event_payload
push_event_payload.commit_title
end
def commit_id
commit_to || commit_from
end
def commits_count
return super unless push_event_payload
push_event_payload.commit_count
end
def validate_push_action
return if action == PUSHED
errors.add(:action, "the action #{action.inspect} is not valid")
end
end

View File

@ -0,0 +1,22 @@
class PushEventPayload < ActiveRecord::Base
include ShaAttribute
belongs_to :event, inverse_of: :push_event_payload
validates :event_id, :commit_count, :action, :ref_type, presence: true
validates :commit_title, length: { maximum: 70 }
sha_attribute :commit_from
sha_attribute :commit_to
enum action: {
created: 0,
removed: 1,
pushed: 2
}
enum ref_type: {
branch: 0,
tag: 1
}
end

View File

@ -224,7 +224,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
begin begin
rugged.references.create(keep_around_ref_name(sha), sha, force: true) write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex rescue Rugged::OSError => ex
@ -237,6 +237,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
@ -987,12 +991,10 @@ class Repository
if start_repository == self if start_repository == self
start_branch_name start_branch_name
else else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" tmp_ref = fetch_ref(
fetch_ref(
start_repository.path_to_repo, start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
tmp_ref "refs/tmp/#{SecureRandom.hex}/head"
) )
start_repository.commit(start_branch_name).sha start_repository.commit(start_branch_name).sha
@ -1023,7 +1025,12 @@ class Repository
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args) message, status = run_git(args)
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
target_ref
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)

View File

@ -726,9 +726,9 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w[username skype linkedin twitter].each do |attr| %i[skype linkedin twitter].each do |attr|
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend value = self[attr]
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend self[attr] = Sanitize.clean(value) if value.present?
end end
end end
@ -1069,6 +1069,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args) def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
devise_mailer.send(notification, self, *args).deliver_later devise_mailer.send(notification, self, *args).deliver_later
end end

View File

@ -85,13 +85,13 @@ module Ci
end end
def register_failure def register_failure
failed_attempt_counter.increase failed_attempt_counter.increment
attempt_counter.increase attempt_counter.increment
end end
def register_success(job) def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
attempt_counter.increase attempt_counter.increment
end end
def failed_attempt_counter def failed_attempt_counter

View File

@ -71,7 +71,14 @@ class EventCreateService
end end
def push(project, current_user, push_data) def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data) # We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being
# rolled back as well.
Event.transaction do
event = create_event(project, current_user, Event::PUSHED)
PushEventPayloadService.new(event, push_data).execute
end
Users::ActivityService.new(current_user, 'push').execute Users::ActivityService.new(current_user, 'push').execute
end end

View File

@ -10,9 +10,11 @@ class NotificationService
# only if ssh key is not deploy key # only if ssh key is not deploy key
# #
# This is security email so it will be sent # This is security email so it will be sent
# even if user disabled notifications # even if user disabled notifications. However,
# it won't be sent to internal users like the
# ghost user or the EE support bot.
def new_key(key) def new_key(key)
if key.user if key.user&.can?(:receive_notifications)
mailer.new_ssh_key_email(key.id).deliver_later mailer.new_ssh_key_email(key.id).deliver_later
end end
end end
@ -22,14 +24,14 @@ class NotificationService
# This is a security email so it will be sent even if the user user disabled # This is a security email so it will be sent even if the user user disabled
# notifications # notifications
def new_gpg_key(gpg_key) def new_gpg_key(gpg_key)
if gpg_key.user if gpg_key.user&.can?(:receive_notifications)
mailer.new_gpg_key_email(gpg_key.id).deliver_later mailer.new_gpg_key_email(gpg_key.id).deliver_later
end end
end end
# Always notify user about email added to profile # Always notify user about email added to profile
def new_email(email) def new_email(email)
if email.user if email.user&.can?(:receive_notifications)
mailer.new_email_email(email.id).deliver_later mailer.new_email_email(email.id).deliver_later
end end
end end
@ -185,6 +187,8 @@ class NotificationService
# Notify new user with email after creation # Notify new user with email after creation
def new_user(user, token = nil) def new_user(user, token = nil)
return true unless notifiable?(user, :mention)
# Don't email omniauth created users # Don't email omniauth created users
mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
end end
@ -206,19 +210,27 @@ class NotificationService
# Members # Members
def new_access_request(member) def new_access_request(member)
return true unless member.notifiable?(:subscription)
mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
end end
def decline_access_request(member) def decline_access_request(member)
return true unless member.notifiable?(:subscription)
mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
end end
# Project invite # Project invite
def invite_project_member(project_member, token) def invite_project_member(project_member, token)
return true unless project_member.notifiable?(:subscription)
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end end
def accept_project_invite(project_member) def accept_project_invite(project_member)
return true unless project_member.notifiable?(:subscription)
mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end end
@ -232,10 +244,14 @@ class NotificationService
end end
def new_project_member(project_member) def new_project_member(project_member)
return true unless project_member.notifiable?(:mention, skip_read_ability: true)
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end end
def update_project_member(project_member) def update_project_member(project_member)
return true unless project_member.notifiable?(:mention)
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end end
@ -249,6 +265,9 @@ class NotificationService
end end
def decline_group_invite(group_member) def decline_group_invite(group_member)
# always send this one, since it's a response to the user's own
# action
mailer.member_invite_declined_email( mailer.member_invite_declined_email(
group_member.real_source_type, group_member.real_source_type,
group_member.group.id, group_member.group.id,
@ -258,15 +277,19 @@ class NotificationService
end end
def new_group_member(group_member) def new_group_member(group_member)
return true unless group_member.notifiable?(:mention)
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end end
def update_group_member(group_member) def update_group_member(group_member)
return true unless group_member.notifiable?(:mention)
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end end
def project_was_moved(project, old_path_with_namespace) def project_was_moved(project, old_path_with_namespace)
recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) recipients = notifiable_users(project.team.members, :mention, project: project)
recipients.each do |recipient| recipients.each do |recipient|
mailer.project_was_moved_email( mailer.project_was_moved_email(
@ -288,10 +311,14 @@ class NotificationService
end end
def project_exported(project, current_user) def project_exported(project, current_user)
return true unless notifiable?(current_user, :mention, project: project)
mailer.project_was_exported_email(current_user, project).deliver_later mailer.project_was_exported_email(current_user, project).deliver_later
end end
def project_not_exported(project, current_user, errors) def project_not_exported(project, current_user, errors)
return true unless notifiable?(current_user, :mention, project: project)
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end end
@ -300,7 +327,7 @@ class NotificationService
return unless mailer.respond_to?(email_template) return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.notifiable_users( recipients ||= notifiable_users(
[pipeline.user], :watch, [pipeline.user], :watch,
custom_action: :"#{pipeline.status}_pipeline", custom_action: :"#{pipeline.status}_pipeline",
target: pipeline target: pipeline
@ -369,7 +396,7 @@ class NotificationService
def relabeled_resource_email(target, labels, current_user, method) def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) } recipients = labels.flat_map { |l| l.subscribers(target.project) }
recipients = NotificationRecipientService.notifiable_users( recipients = notifiable_users(
recipients, :subscription, recipients, :subscription,
target: target, target: target,
acting_user: current_user acting_user: current_user
@ -401,4 +428,14 @@ class NotificationService
object.previous_changes[attribute].first object.previous_changes[attribute].first
end end
end end
private
def notifiable?(*args)
NotificationRecipientService.notifiable?(*args)
end
def notifiable_users(*args)
NotificationRecipientService.notifiable_users(*args)
end
end end

View File

@ -128,6 +128,8 @@ module Projects
project.repository.before_delete project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete
Projects::ForksCountService.new(project).delete_cache
end end
end end
end end

View File

@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
new_project new_project
end end
private private
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
def allowed_visibility_level def allowed_visibility_level
project_level = @project.visibility_level project_level = @project.visibility_level

View File

@ -0,0 +1,30 @@
module Projects
# Service class for getting and caching the number of forks of a project.
class ForksCountService
def initialize(project)
@project = project
end
def count
Rails.cache.fetch(cache_key) { uncached_count }
end
def refresh_cache
Rails.cache.write(cache_key, uncached_count)
end
def delete_cache
Rails.cache.delete(cache_key)
end
private
def uncached_count
@project.forks.count
end
def cache_key
['projects', @project.id, 'forks_count']
end
end
end

View File

@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr) ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end end
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy @project.forked_project_link.destroy
end end
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
end end
end end

View File

@ -172,11 +172,11 @@ module Projects
end end
def register_attempt def register_attempt
pages_deployments_total_counter.increase pages_deployments_total_counter.increment
end end
def register_failure def register_failure
pages_deployments_failed_total_counter.increase pages_deployments_failed_total_counter.increment
end end
def pages_deployments_total_counter def pages_deployments_total_counter

View File

@ -0,0 +1,120 @@
# Service class for creating push event payloads as stored in the
# "push_event_payloads" table.
#
# Example:
#
# data = Gitlab::DataBuilder::Push.build(...)
# event = Event.create(...)
#
# PushEventPayloadService.new(event, data).execute
class PushEventPayloadService
# event - The event this push payload belongs to.
# push_data - A Hash produced by `Gitlab::DataBuilder::Push.build` to use for
# building the push payload.
def initialize(event, push_data)
@event = event
@push_data = push_data
end
# Creates and returns a new PushEventPayload row.
#
# This method will raise upon encountering validation errors.
#
# Returns an instance of PushEventPayload.
def execute
@event.build_push_event_payload(
commit_count: commit_count,
action: action,
ref_type: ref_type,
commit_from: commit_from_id,
commit_to: commit_to_id,
ref: trimmed_ref,
commit_title: commit_title,
event_id: @event.id
)
@event.push_event_payload.save!
@event.push_event_payload
end
# Returns the commit title to use.
#
# The commit title is limited to the first line and a maximum of 70
# characters.
def commit_title
commit = @push_data.fetch(:commits).last
return nil unless commit && commit[:message]
raw_msg = commit[:message]
# Find where the first line ends, without turning the entire message into an
# Array of lines (this is a waste of memory for large commit messages).
index = raw_msg.index("\n")
message = index ? raw_msg[0..index] : raw_msg
message.strip.truncate(70)
end
def commit_from_id
if create?
nil
else
revision_before
end
end
def commit_to_id
if remove?
nil
else
revision_after
end
end
def commit_count
@push_data.fetch(:total_commits_count)
end
def ref
@push_data.fetch(:ref)
end
def revision_before
@push_data.fetch(:before)
end
def revision_after
@push_data.fetch(:after)
end
def trimmed_ref
Gitlab::Git.ref_name(ref)
end
def create?
Gitlab::Git.blank_ref?(revision_before)
end
def remove?
Gitlab::Git.blank_ref?(revision_after)
end
def action
if create?
:created
elsif remove?
:removed
else
:pushed
end
end
def ref_type
if Gitlab::Git.tag_ref?(ref)
:tag
else
:branch
end
end
end

View File

@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
end end
def self.base_dir def self.base_dir
File.join(root_dir, 'system') File.join(root_dir, '-', 'system')
end end
private private

View File

@ -48,6 +48,12 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help %span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access. Allow only the selected protocols to be used for Git access.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :project_export_enabled do
= f.check_box :project_export_enabled
Project export enabled
%fieldset %fieldset
%legend Account and Limit Settings %legend Account and Limit Settings

View File

@ -28,6 +28,6 @@
%h3.blank-state-title %h3.blank-state-title
Create a group Create a group
%p.blank-state-text %p.blank-state-text
Groups are a great way to organise projects and people. Groups are a great way to organize projects and people.
= link_to new_group_path, class: "btn btn-new" do = link_to new_group_path, class: "btn btn-new" do
New group New group

View File

@ -1,5 +1,5 @@
%li.commit %li.commit
.commit-row-title .commit-row-title
= link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit-sha", alt: '', title: truncate_sha(commit[:id]) = link_to truncate_sha(event.commit_id), project_commit_path(project, event.commit_id), class: "commit-sha", alt: '', title: truncate_sha(event.commit_id)
&middot; &middot;
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author = markdown event_commit_title(event.commit_title), project: project, pipeline: :single_line, author: event.author

View File

@ -1,14 +1,13 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" } %div{ xmlns: "http://www.w3.org/1999/xhtml" }
- event.commits.first(15).each do |commit|
%p %p
%strong= commit[:author][:name] %strong= event.author_name
= link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id)
%i %i
at at
= commit[:timestamp].to_time.to_s(:short) = event.created_at.to_s(:short)
%blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author) %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
- if event.commits_count > 15 - if event.commits_count > 1
%p %p
%i %i
\... and \... and
= pluralize(event.commits_count - 15, "more commit") = pluralize(event.commits_count - 1, "more commit")

View File

@ -14,9 +14,7 @@
- if event.push_with_commits? - if event.push_with_commits?
.event-body .event-body
%ul.well-list.event_commits %ul.well-list.event_commits
- few_commits = event.commits[0...2] = render "events/commit", project: project, event: event
- few_commits.each do |commit|
= render "events/commit", commit: commit, project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1 - if event.commits_count > 1
@ -44,9 +42,6 @@
= link_to create_mr_path(project.default_branch, event.ref_name, project) do = link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request Create Merge Request
- elsif event.rm_ref? - elsif event.rm_ref?
- repository = project.repository
- last_commit = repository.commit(event.commit_from)
- if last_commit
.event-body .event-body
%ul.well-list.event_commits %ul.well-list.event_commits
= render "events/commit", commit: last_commit, project: project, event: event = render "events/commit", project: project, event: event

View File

@ -10,7 +10,7 @@
Customize how FogBugz email addresses and usernames are imported into GitLab. Customize how FogBugz email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import. In the next step, you'll be able to select the projects you want to import.
%p %p
The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below. The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames will be imported into GitLab. You can change this by populating the table below.
%ul %ul
%li %li
%strong Default: Map a FogBugz account ID to a full name %strong Default: Map a FogBugz account ID to a full name

View File

@ -0,0 +1,8 @@
.gl-pagination
%ul.pagination.clearfix
- if previous_path
%li.prev
= link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
- if next_path
%li.next
= link_to(t('views.pagination.next'), next_path, rel: 'next')

View File

@ -0,0 +1,41 @@
- return unless current_application_settings.project_export_enabled?
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
%section.settings
.settings-header
%h4
Export project
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
.settings-content.no-animate{ class: ('expanded' if expanded) }
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
The following items will be exported:
%ul
%li Project and wiki repositories
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
%li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link.
- if project.export_project_path
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project),
method: :post, class: "btn btn-default"
- else
= link_to 'Export project', export_project_path(project),
method: :post, class: "btn btn-default"

View File

@ -1,11 +1,9 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil) - referenced_users = local_assigns.fetch(:referenced_users, nil)
- if defined?(@issue) && @issue.confidential? - if defined?(@issue) && @issue.confidential?
%li.confidential-issue-warning .confidential-issue-warning
= confidential_icon(@issue) = confidential_icon(@issue)
%span This is a confidential issue. Your comment will not be visible to the public. %span This is a confidential issue. Your comment will not be visible to the public.
- else
%li.confidential-issue-warning.not-confidential
.md-area .md-area
.md-header .md-header

View File

@ -1,3 +1,2 @@
- if commit.has_signature? - if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
%i.fa.fa-spinner.fa-spin

View File

@ -39,6 +39,9 @@
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
%li %li
%a{ "href" => "#", "data-value" => "30" } %a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 30) }} {{ n__('Last %d day', 'Last %d days', 30) }}

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