Commit f2326440 authored by Regis's avatar Regis

resolve conflict

parents 4c9e68cb 8100f968
...@@ -11,6 +11,7 @@ variables: ...@@ -11,6 +11,7 @@ variables:
NODE_ENV: "test" NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
...@@ -18,8 +19,8 @@ variables: ...@@ -18,8 +19,8 @@ variables:
before_script: before_script:
- bundle --version - bundle --version
- . scripts/utils.sh - source scripts/utils.sh
- ./scripts/prepare_build.sh - source scripts/prepare_build.sh
stages: stages:
- prepare - prepare
...@@ -59,7 +60,7 @@ stages: ...@@ -59,7 +60,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
- /\-(?i)mysql$/ - /mysql/
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
...@@ -67,6 +68,13 @@ stages: ...@@ -67,6 +68,13 @@ stages:
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
- //@gitlab/gitlab-ee - //@gitlab/gitlab-ee
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs: &except-docs
except:
- /^docs\/.*/
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -90,11 +98,13 @@ stages: ...@@ -90,11 +98,13 @@ stages:
.rspec-knapsack-pg: &rspec-knapsack-pg .rspec-knapsack-pg: &rspec-knapsack-pg
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *use-pg <<: *use-pg
<<: *except-docs
.rspec-knapsack-mysql: &rspec-knapsack-mysql .rspec-knapsack-mysql: &rspec-knapsack-mysql
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *use-mysql <<: *use-mysql
<<: *only-master-and-ee-or-mysql <<: *only-master-and-ee-or-mysql
<<: *except-docs
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
...@@ -119,16 +129,19 @@ stages: ...@@ -119,16 +129,19 @@ stages:
.spinach-knapsack-pg: &spinach-knapsack-pg .spinach-knapsack-pg: &spinach-knapsack-pg
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *use-pg <<: *use-pg
<<: *except-docs
.spinach-knapsack-mysql: &spinach-knapsack-mysql .spinach-knapsack-mysql: &spinach-knapsack-mysql
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *use-mysql <<: *use-mysql
<<: *only-master-and-ee-or-mysql <<: *only-master-and-ee-or-mysql
<<: *except-docs
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
<<: *knapsack-state <<: *knapsack-state
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: prepare stage: prepare
script: script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
...@@ -155,6 +168,7 @@ update-knapsack: ...@@ -155,6 +168,7 @@ update-knapsack:
setup-test-env: setup-test-env:
<<: *use-pg <<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: prepare stage: prepare
script: script:
- node --version - node --version
...@@ -239,35 +253,32 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -239,35 +253,32 @@ spinach mysql 9 10: *spinach-knapsack-mysql
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
.exec: &exec .rake-exec: &rake-exec
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- bundle exec $CI_JOB_NAME - bundle exec rake $CI_JOB_NAME
rubocop: static-analysis:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- bundle exec "rubocop --require rubocop-rspec" - scripts/static-analysis
rake haml_lint: *exec downtime_check:
rake scss_lint: *exec <<: *rake-exec
rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
rake downtime_check:
<<: *exec
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?$/
- /^docs\/*/
rake ee_compat_check: ee_compat_check:
<<: *exec <<: *rake-exec
only: only:
- branches@gitlab-org/gitlab-ce - branches@gitlab-org/gitlab-ce
except: except:
...@@ -292,13 +303,15 @@ rake ee_compat_check: ...@@ -292,13 +303,15 @@ rake ee_compat_check:
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
rake pg db:migrate:reset: db:migrate:reset pg:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:migrate:reset: db:migrate:reset mysql:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback .db-rollback: &db-rollback
stage: test stage: test
...@@ -307,13 +320,15 @@ rake mysql db:migrate:reset: ...@@ -307,13 +320,15 @@ rake mysql db:migrate:reset:
- bundle exec rake db:rollback STEP=120 - bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate - bundle exec rake db:migrate
rake pg db:rollback: db:rollback pg:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:rollback: db:rollback mysql:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
stage: test stage: test
...@@ -332,17 +347,20 @@ rake mysql db:rollback: ...@@ -332,17 +347,20 @@ rake mysql db:rollback:
paths: paths:
- log/development.log - log/development.log
rake pg db:seed_fu: db:seed_fu pg:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:seed_fu: db:seed_fu mysql:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
<<: *except-docs
rake gitlab:assets:compile: gitlab:assets:compile:
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
dependencies: [] dependencies: []
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
...@@ -359,13 +377,14 @@ rake gitlab:assets:compile: ...@@ -359,13 +377,14 @@ rake gitlab:assets:compile:
paths: paths:
- webpack-report/ - webpack-report/
rake karma: karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
stage: test stage: test
<<: *use-pg <<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
script: script:
...@@ -377,16 +396,6 @@ rake karma: ...@@ -377,16 +396,6 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
docs:check:apilint:
image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
docs:check:links: docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test stage: test
...@@ -402,13 +411,6 @@ docs:check:links: ...@@ -402,13 +411,6 @@ docs:check:links:
# Check the internal links # Check the internal links
- bundle exec nanoc check internal_links - bundle exec nanoc check internal_links
bundler:check:
stage: test
<<: *dedicated-runner
<<: *ruby-static-analysis
script:
- bundle check
bundler:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -441,11 +443,11 @@ bundler:audit: ...@@ -441,11 +443,11 @@ bundler:audit:
- . scripts/prepare_build.sh - . scripts/prepare_build.sh
- bundle exec rake db:migrate - bundle exec rake db:migrate
migration pg paths: migration path pg:
<<: *migration-paths <<: *migration-paths
<<: *use-pg <<: *use-pg
migration mysql paths: migration path mysql:
<<: *migration-paths <<: *migration-paths
<<: *use-mysql <<: *use-mysql
...@@ -453,6 +455,7 @@ coverage: ...@@ -453,6 +455,7 @@ coverage:
stage: post-test stage: post-test
services: [] services: []
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
...@@ -466,15 +469,9 @@ coverage: ...@@ -466,15 +469,9 @@ coverage:
- coverage/index.html - coverage/index.html
- coverage/assets/ - coverage/assets/
lint:javascript:
<<: *dedicated-runner
stage: test
before_script: []
script:
- yarn run eslint
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: post-test stage: post-test
before_script: [] before_script: []
script: script:
...@@ -527,8 +524,8 @@ pages: ...@@ -527,8 +524,8 @@ pages:
<<: *dedicated-runner <<: *dedicated-runner
dependencies: dependencies:
- coverage - coverage
- rake karma - karma
- rake gitlab:assets:compile - gitlab:assets:compile
- lint:javascript:report - lint:javascript:report
script: script:
- mv public/ .public/ - mv public/ .public/
......
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "regression" or "bug" label:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Summary ### Summary
(Summarize the bug encountered concisely) (Summarize the bug encountered concisely)
...@@ -26,6 +40,7 @@ logs, and code as it's very hard to read otherwise.) ...@@ -26,6 +40,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info #### Results of GitLab environment info
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`) `sudo gitlab-rake gitlab:env:info`)
...@@ -33,11 +48,13 @@ logs, and code as it's very hard to read otherwise.) ...@@ -33,11 +48,13 @@ logs, and code as it's very hard to read otherwise.)
(For installations from source run and paste the output of: (For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</pre>
</details> </details>
#### Results of GitLab application Check #### Results of GitLab application Check
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`) `sudo gitlab-rake gitlab:check SANITIZE=true`)
...@@ -47,8 +64,11 @@ logs, and code as it's very hard to read otherwise.) ...@@ -47,8 +64,11 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing) (we will only investigate if the tests are passing)
</pre>
</details> </details>
### Possible fixes ### Possible fixes
(If you can, link to the line of code that might be responsible for the problem) (If you can, link to the line of code that might be responsible for the problem)
/label ~bug
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
filtered by the "feature proposal" label:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Description ### Description
(Include problem, use cases, benefits, and/or goals) (Include problem, use cases, benefits, and/or goals)
...@@ -15,3 +28,5 @@ ...@@ -15,3 +28,5 @@
3. How does someone use this 3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.) During implementation, this can then be copied and used as a starter for the documentation.)
/label ~"feature proposal"
...@@ -543,7 +543,7 @@ Style/Proc: ...@@ -543,7 +543,7 @@ Style/Proc:
# branches, and conditions. # branches, and conditions.
Metrics/AbcSize: Metrics/AbcSize:
Enabled: true Enabled: true
Max: 60 Max: 57.08
# This cop checks if the length of a block exceeds some maximum value. # This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength: Metrics/BlockLength:
...@@ -562,7 +562,7 @@ Metrics/ClassLength: ...@@ -562,7 +562,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method. # of test cases needed to validate a method.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Enabled: true Enabled: true
Max: 17 Max: 16
# Limit lines to 80 characters. # Limit lines to 80 characters.
Metrics/LineLength: Metrics/LineLength:
...@@ -983,10 +983,12 @@ RSpec/ExpectActual: ...@@ -983,10 +983,12 @@ RSpec/ExpectActual:
# Checks the file and folder naming of the spec file. # Checks the file and folder naming of the spec file.
RSpec/FilePath: RSpec/FilePath:
Enabled: false Enabled: true
CustomTransform: IgnoreMethods: true
RuboCop: rubocop Exclude:
RSpec: rspec - 'qa/**/*'
- 'spec/javascripts/fixtures/*'
- 'spec/requests/api/v3/*'
# Checks if there are focused specs. # Checks if there are focused specs.
RSpec/Focus: RSpec/Focus:
......
...@@ -2,6 +2,32 @@ ...@@ -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.1.2 (2017-05-01)
- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
- Fix pipeline events description for Slack and Mattermost integration. !10908
- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933
- Fix ordering of commits in the network graph. !10936
- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959
- Lazily sets UUID in ApplicationSetting for new installations.
- Skip validation when creating internal (ghost, service desk) users.
- Use GitLab Pages v0.4.1.
## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465
- Properly expire cache for all MRs of a pipeline. !10770
- Add sub-nav for Project Integration Services edit page. !10813
- Fix missing duration for blocked pipelines. !10856
- Fix lastest commit status text on main project page. !10863
- Add index on ci_builds.updated_at. !10870 (blackst0ne)
- Fix 500 error due to trying to show issues from pending deleting projects. !10906
- Ensures that OAuth/LDAP/SAML users don't need to be confirmed.
- Ensure replying to an individual note by email creates a note with its own discussion ID.
- Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled.
- Fix usage ping docs link from empty cohorts page.
- Eliminate N+1 queries in loading namespaces for every issuable in milestones.
## 9.1.0 (2017-04-22) ## 9.1.0 (2017-04-22)
- Added merge requests empty state. !7342 - Added merge requests empty state. !7342
......
...@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres ...@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1' gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
...@@ -83,14 +85,14 @@ gem 'kaminari', '~> 0.17.0' ...@@ -83,14 +85,14 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
# Files attachments # Files attachments
gem 'carrierwave', '~> 0.11.0' gem 'carrierwave', '~> 1.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5' gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
...@@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2.7' gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4' gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
...@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2' ...@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
gem 'slack-notifier', '~> 1.5.1' gem 'slack-notifier', '~> 1.5.1'
# Asana integration # Asana integration
gem 'asana', '~> 0.4.0' gem 'asana', '~> 0.6.0'
# FogBugz integration # FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
...@@ -291,6 +293,7 @@ group :development, :test do ...@@ -291,6 +293,7 @@ group :development, :test do
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
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'
# 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'
...@@ -345,7 +348,7 @@ gem 'html2text' ...@@ -345,7 +348,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
# OAuth # OAuth
gem 'oauth2', '~> 1.2.0' gem 'oauth2', '~> 1.3.0'
# Soft deletion # Soft deletion
gem 'paranoia', '~> 2.2' gem 'paranoia', '~> 2.2'
......
...@@ -47,7 +47,7 @@ GEM ...@@ -47,7 +47,7 @@ GEM
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.4) arel (6.0.4)
asana (0.4.0) asana (0.6.0)
faraday (~> 0.9) faraday (~> 0.9)
faraday_middleware (~> 0.9) faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
...@@ -105,12 +105,10 @@ GEM ...@@ -105,12 +105,10 @@ GEM
capybara-screenshot (1.0.14) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (0.11.2) carrierwave (1.0.0)
activemodel (>= 3.2.0) activemodel (>= 4.0.0)
activesupport (>= 3.2.0) activesupport (>= 4.0.0)
json (>= 1.7)
mime-types (>= 1.16) mime-types (>= 1.16)
mimemagic (>= 0.3.0)
cause (0.1) cause (0.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chronic (0.10.2) chronic (0.10.2)
...@@ -184,7 +182,7 @@ GEM ...@@ -184,7 +182,7 @@ GEM
erubis (2.7.0) erubis (2.7.0)
escape_utils (1.1.1) escape_utils (1.1.1)
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.52.0) excon (0.55.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extlib (0.9.16) extlib (0.9.16)
...@@ -193,10 +191,10 @@ GEM ...@@ -193,10 +191,10 @@ GEM
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.9.2) faraday (0.11.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0) faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 0.10) faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
...@@ -210,12 +208,12 @@ GEM ...@@ -210,12 +208,12 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aws (0.11.0) fog-aws (0.13.0)
fog-core (~> 1.38) fog-core (~> 1.38)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-core (1.42.0) fog-core (1.44.1)
builder builder
excon (~> 0.49) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
...@@ -237,9 +235,9 @@ GEM ...@@ -237,9 +235,9 @@ GEM
fog-json (>= 1.0) fog-json (>= 1.0)
fog-xml (>= 0.1) fog-xml (>= 0.1)
ipaddress (>= 0.8) ipaddress (>= 0.8)
fog-xml (0.1.2) fog-xml (0.1.3)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1) font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
...@@ -330,7 +328,7 @@ GEM ...@@ -330,7 +328,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.1.2) grpc (1.2.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
...@@ -429,7 +427,7 @@ GEM ...@@ -429,7 +427,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.5)
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)
...@@ -454,15 +452,15 @@ GEM ...@@ -454,15 +452,15 @@ GEM
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.2.0) oauth2 (1.3.1)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.12)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.5)
omniauth (1.4.2) omniauth (1.4.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
...@@ -603,7 +601,7 @@ GEM ...@@ -603,7 +601,7 @@ GEM
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.2.2) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
...@@ -659,6 +657,7 @@ GEM ...@@ -659,6 +657,7 @@ GEM
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-set (0.1.3)
rspec-support (3.5.0) rspec-support (3.5.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
activerecord activerecord
...@@ -716,11 +715,11 @@ GEM ...@@ -716,11 +715,11 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.2.10) sidekiq (5.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4) sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
...@@ -853,7 +852,7 @@ DEPENDENCIES ...@@ -853,7 +852,7 @@ DEPENDENCIES
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.4.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7) asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
...@@ -870,7 +869,7 @@ DEPENDENCIES ...@@ -870,7 +869,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.6.2) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.11.0) carrierwave (~> 1.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
...@@ -891,10 +890,11 @@ DEPENDENCIES ...@@ -891,10 +890,11 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0)
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.8.0) flay (~> 2.8.0)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.40) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
...@@ -943,7 +943,7 @@ DEPENDENCIES ...@@ -943,7 +943,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16) mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1) net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0) oauth2 (~> 1.3.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
...@@ -988,6 +988,7 @@ DEPENDENCIES ...@@ -988,6 +988,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1) rubocop (~> 0.47.1)
rubocop-rspec (~> 1.15.0) rubocop-rspec (~> 1.15.0)
...@@ -1004,7 +1005,7 @@ DEPENDENCIES ...@@ -1004,7 +1005,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7) sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0) simplecov (~> 0.14.0)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage ## Test coverage
......
...@@ -31,7 +31,7 @@ export default () => { ...@@ -31,7 +31,7 @@ export default () => {
}, },
}, },
template: ` template: `
<div class="container-fluid md prepend-top-default append-bottom-default"> <div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
<div <div
class="text-center loading" class="text-center loading"
v-if="loading && !error"> v-if="loading && !error">
......
/* global Flash */
export default class BlobViewer {
constructor() {
this.switcher = document.querySelector('.js-blob-viewer-switcher');
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$fileHolder = $('.file-holder');
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
this.initBindings();
if (this.switcher && location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
this.switchToViewer(initialViewerName);
}
initBindings() {
if (this.switcherBtns.length) {
Array.from(this.switcherBtns)
.forEach((el) => {
el.addEventListener('click', this.switchViewHandler.bind(this));
});
}
if (this.copySourceBtn) {
this.copySourceBtn.addEventListener('click', () => {
if (this.copySourceBtn.classList.contains('disabled')) return;
this.switchToViewer('simple');
});
}
}
switchViewHandler(e) {
const target = e.currentTarget;
e.preventDefault();
this.switchToViewer(target.getAttribute('data-viewer'));
}
toggleCopyButtonState() {
if (!this.copySourceBtn) return;
if (this.simpleViewer.getAttribute('data-loaded')) {
this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
this.copySourceBtn.classList.add('disabled');
} else {
this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
this.copySourceBtn.classList.add('disabled');
}
$(this.copySourceBtn).tooltip('fixTitle');
}
loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading source view'))
.done((data) => {
viewer.innerHTML = data.html;
$(viewer).syntaxHighlight();
viewer.setAttribute('data-loaded', 'true');
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
});
}
switchToViewer(name) {
const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
if (oldButton) {
oldButton.classList.remove('active');
}
if (newButton) {
newButton.classList.add('active');
newButton.blur();
}
if (oldViewer) {
oldViewer.classList.add('hidden');
}
newViewer.classList.remove('hidden');
this.activeViewer = newViewer;
this.toggleCopyButtonState();
this.loadViewer(newViewer);
}
}
...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', { ...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
...@@ -48,6 +48,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -48,6 +48,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki'; import ShortcutsWiki from './shortcuts_wiki';
import BlobViewer from './blob/viewer/index';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -299,6 +300,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -299,6 +300,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
gl.TargetBranchDropDown.bootstrap(); gl.TargetBranchDropDown.bootstrap();
break; break;
case 'projects:blob:show': case 'projects:blob:show':
new BlobViewer();
gl.TargetBranchDropDown.bootstrap(); gl.TargetBranchDropDown.bootstrap();
initBlob(); initBlob();
break; break;
...@@ -354,6 +356,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -354,6 +356,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'users:show': case 'users:show':
new UserCallout(); new UserCallout();
break; break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'sessions': case 'sessions':
...@@ -432,6 +438,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -432,6 +438,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
if (path[2] === 'show') { if (path[2] === 'show') {
new ZenMode(); new ZenMode();
new LineHighlighter();
new BlobViewer();
} }
break; break;
case 'labels': case 'labels':
......
<script>
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue'; import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
...@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati ...@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default Vue.component('environment-component', { export default {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
...@@ -140,76 +141,90 @@ export default Vue.component('environment-component', { ...@@ -140,76 +141,90 @@ export default Vue.component('environment-component', {
}); });
}, },
}, },
};
template: ` </script>
<div :class="cssContainerClass"> <template>
<div class="top-area"> <div :class="cssContainerClass">
<ul v-if="!isLoading" class="nav-links"> <div class="top-area">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }"> <ul
<a :href="projectEnvironmentsPath"> v-if="!isLoading"
Available class="nav-links">
<span class="badge js-available-environments-count"> <li :class="{ active: scope === null || scope === 'available' }">
{{state.availableCounter}} <a :href="projectEnvironmentsPath">
</span> Available
</a> <span class="badge js-available-environments-count">
</li> {{state.availableCounter}}
<li v-bind:class="{ 'active' : scope === 'stopped' }"> </span>
<a :href="projectStoppedEnvironmentsPath"> </a>
Stopped </li>
<span class="badge js-stopped-environments-count"> <li :class="{ active : scope === 'stopped' }">
{{state.stoppedCounter}} <a :href="projectStoppedEnvironmentsPath">
</span> Stopped
</a> <span class="badge js-stopped-environments-count">
</li> {{state.stoppedCounter}}
</ul> </span>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
</a> </a>
</div> </li>
</ul>
<div
v-if="canCreateEnvironmentParsed && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
New environment
</a>
</div> </div>
</div>
<div class="content-list environments-container">
<div
class="environments-list-loading text-center"
v-if="isLoading">
<div class="content-list environments-container"> <i
<div class="environments-list-loading text-center" v-if="isLoading"> class="fa fa-spinner fa-spin"
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> aria-hidden="true" />
</div> </div>
<div class="blank-state blank-state-no-icon" <div
v-if="!isLoading && state.environments.length === 0"> class="blank-state blank-state-no-icon"
<h2 class="blank-state-title js-blank-state-title"> v-if="!isLoading && state.environments.length === 0">
You don't have any environments right now. <h2 class="blank-state-title js-blank-state-title">
</h2> You don't have any environments right now.
<p class="blank-state-text"> </h2>
Environments are places where code gets deployed, such as staging or production. <p class="blank-state-text">
<br /> Environments are places where code gets deployed, such as staging or production.
<a :href="helpPagePath"> <br />
Read more about environments <a :href="helpPagePath">
</a> Read more about environments
</p>
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a> </a>
</div> </p>
<div class="table-holder" <a
v-if="!isLoading && state.environments.length > 0"> v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
<environment-table class="btn btn-create js-new-environment-button">
:environments="state.environments" New Environment
:can-create-deployment="canCreateDeploymentParsed" </a>
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div> </div>
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation" />
</div> </div>
`, </div>
}); </template>
import EnvironmentsComponent from './components/environment'; import Vue from 'vue';
import EnvironmentsComponent from './components/environment.vue';
$(() => { document.addEventListener('DOMContentLoaded', () => new Vue({
window.gl = window.gl || {}; el: '#environments-list-view',
components: {
if (gl.EnvironmentsListApp) { 'environments-table-app': EnvironmentsComponent,
gl.EnvironmentsListApp.$destroy(true); },
} render: createElement => createElement('environments-table-app'),
}));
gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
});
import EnvironmentsFolderComponent from './environments_folder_view'; import Vue from 'vue';
import EnvironmentsFolderComponent from './environments_folder_view.vue';
$(() => { document.addEventListener('DOMContentLoaded', () => new Vue({
window.gl = window.gl || {}; el: '#environments-folder-list-view',
components: {
if (gl.EnvironmentsListFolderApp) { 'environments-folder-app': EnvironmentsFolderComponent,
gl.EnvironmentsListFolderApp.$destroy(true); },
} render: createElement => createElement('environments-folder-app'),
}));
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
<script>
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
...@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati ...@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', { export default {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': TablePaginationComponent, 'table-pagination': TablePaginationComponent,
...@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', { ...@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', {
return param; return param;
}, },
}, },
};
</script>
<template>
<div :class="cssContainerClass">
<div
class="top-area"
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a
:href="availablePath"
class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a
:href="stoppedPath"
class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
template: ` <div class="environments-container">
<div :class="cssContainerClass"> <div
<div class="top-area" v-if="!isLoading"> class="environments-list-loading text-center"
v-if="isLoading">
<h4 class="js-folder-name environments-folder-name"> <i
Environments / <b>{{folderName}}</b> class="fa fa-spinner fa-spin"
</h4> aria-hidden="true"/>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div> </div>
<div class="environments-container"> <div
<div class="environments-list-loading text-center" v-if="isLoading"> class="table-holder"
<i class="fa fa-spinner fa-spin"></i> v-if="!isLoading && state.environments.length > 0">
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table <environment-table
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:service="service"/> :service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination
:change="changePage" v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:pageInfo="state.paginationInformation"/> :change="changePage"
</div> :pageInfo="state.paginationInformation"/>
</div> </div>
</div> </div>
`, </div>
}); </template>
...@@ -77,13 +77,14 @@ class FilteredSearchManager { ...@@ -77,13 +77,14 @@ class FilteredSearchManager {
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.removeTokenWrapper = this.removeToken.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
...@@ -96,12 +97,13 @@ class FilteredSearchManager { ...@@ -96,12 +97,13 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -117,12 +119,13 @@ class FilteredSearchManager { ...@@ -117,12 +119,13 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -195,14 +198,28 @@ class FilteredSearchManager { ...@@ -195,14 +198,28 @@ class FilteredSearchManager {
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (button) { if (!removeButtonSelected && button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
} }
} }
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
e.stopPropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
this.removeSelectedToken();
}
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
...@@ -248,16 +265,21 @@ class FilteredSearchManager { ...@@ -248,16 +265,21 @@ class FilteredSearchManager {
} }
} }
removeSelectedToken(e) { removeSelectedTokenKeydown(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); this.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
} }
} }
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();
......
...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens { ...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
static selectToken(tokenButton) { static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected'); const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens(); FilteredSearchVisualTokens.unselectTokens();
if (!selected) { if (!selected || forceSelection) {
tokenButton.classList.add('selected'); tokenButton.classList.add('selected');
} }
} }
...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens { ...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return ` return `
<div class="selectable" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value"></div> <div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;
} }
...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens { ...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) { if (value) {
const button = lastVisualToken.querySelector('.selectable'); const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value); const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML; lastVisualToken.innerHTML = button.innerHTML;
} else { } else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji'; import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
// Creates the variables for setting up GFM auto-completion // Creates the variables for setting up GFM auto-completion
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = { ...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert, beforeInsert: this.DefaultOptions.beforeInsert,
filter: this.DefaultOptions.filter filter: this.DefaultOptions.filter,
matcher: (flag, subtext) => {
const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(relevantText);
return match && match.length ? match[1] : null;
}
} }
}); });
// Team Members // Team Members
......
/**
* Regexp utility for the convenience of working with regular expressions.
*
*/
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
// Unicode 6.1
const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
export default { unicodeLetters };
...@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo'); ...@@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
LineHighlighter.prototype._hash = ''; LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) { function LineHighlighter(hash) {
var range;
if (hash == null) { if (hash == null) {
// Initialize a LineHighlighter object // Initialize a LineHighlighter object
// //
...@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo'); ...@@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
this.setHash = bind(this.setHash, this); this.setHash = bind(this.setHash, this);
this.highlightLine = bind(this.highlightLine, this); this.highlightLine = bind(this.highlightLine, this);
this.clickHandler = bind(this.clickHandler, this); this.clickHandler = bind(this.clickHandler, this);
this.highlightHash = this.highlightHash.bind(this);
this._hash = hash; this._hash = hash;
this.bindEvents(); this.bindEvents();
if (hash !== '') { this.highlightHash();
range = this.hashToRange(hash); }
LineHighlighter.prototype.bindEvents = function() {
const $fileHolder = $('.file-holder');
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash);
};
LineHighlighter.prototype.highlightHash = function() {
var range;
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) { if (range[0]) {
this.highlightRange(range); this.highlightRange(range);
$.scrollTo("#L" + range[0], { $.scrollTo("#L" + range[0], {
...@@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo'); ...@@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo');
}); });
} }
} }
}
LineHighlighter.prototype.bindEvents = function() {
$('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
}; };
LineHighlighter.prototype.clickHandler = function(event) { LineHighlighter.prototype.clickHandler = function(event) {
......
...@@ -275,7 +275,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -275,7 +275,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
suggestionSections: $(el).find('.js-file-fork-suggestion-section'), suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
}); })
.init();
}); });
}, },
}); });
......
...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph { ...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph {
* All dropdown events are fired at the .dropdown-menu's parent element. * All dropdown events are fired at the .dropdown-menu's parent element.
*/ */
bindEvents() { bindEvents() {
$(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); $(document)
.off('shown.bs.dropdown', this.container)
.on('shown.bs.dropdown', this.container, this.getBuildsList);
} }
/** /**
...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph { ...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph {
}, },
error: () => { error: () => {
this.toggleLoading(button); this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
new Flash('An error occurred while fetching the builds.', 'alert'); new Flash('An error occurred while fetching the builds.', 'alert');
}, },
}); });
......
...@@ -2,13 +2,6 @@ ...@@ -2,13 +2,6 @@
import StatusIconEntityMap from '../../ci_status_icons'; import StatusIconEntityMap from '../../ci_status_icons';
export default { export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: { props: {
stage: { stage: {
type: Object, type: Object,
...@@ -16,6 +9,13 @@ export default { ...@@ -16,6 +9,13 @@ export default {
}, },
}, },
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
updated() { updated() {
if (this.builds) { if (this.builds) {
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
...@@ -31,7 +31,13 @@ export default { ...@@ -31,7 +31,13 @@ export default {
return this.$http.get(this.stage.dropdown_path) return this.$http.get(this.stage.dropdown_path)
.then((response) => { .then((response) => {
this.builds = JSON.parse(response.body).html; this.builds = JSON.parse(response.body).html;
}, () => { })
.catch(() => {
// If dropdown is opened we'll close it.
if (this.$el.classList.contains('open')) {
$(this.$refs.dropdown).dropdown('toggle');
}
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
return flash; return flash;
}); });
...@@ -46,9 +52,10 @@ export default { ...@@ -46,9 +52,10 @@ export default {
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
e.stopPropagation(); .on('click', (e) => {
}); e.stopPropagation();
});
}, },
}, },
computed: { computed: {
...@@ -81,12 +88,22 @@ export default { ...@@ -81,12 +88,22 @@ export default {
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
type="button" type="button"
:aria-label="stage.title"> :aria-label="stage.title"
<span v-html="svgHTML" aria-hidden="true"></span> ref="dropdown">
<i class="fa fa-caret-down" aria-hidden="true"></i> <span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul
<div class="arrow-up" aria-hidden="true"></div> ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
<div <div
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
......
...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg'; ...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility'; import '../../lib/utils/datetime_utility';
export default { export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
currentTime: new Date(),
iconTimerSvg, iconTimerSvg,
}; };
}, },
props: ['pipeline'],
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: { computed: {
timeAgo() { hasDuration() {
return gl.utils.getTimeago(); return this.duration > 0;
}, },
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at); hasFinishedTime() {
return this.finishedTime !== '';
}, },
timeStopped() {
const changeTime = this.currentTime; localTimeFinished() {
const options = { return gl.utils.formatDate(this.finishedTime);
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
}, },
duration() {
const { duration } = this.pipeline.details; durationFormated() {
const date = new Date(duration * 1000); const date = new Date(this.duration * 1000);
let hh = date.getUTCHours(); let hh = date.getUTCHours();
let mm = date.getUTCMinutes(); let mm = date.getUTCMinutes();
let ss = date.getSeconds(); let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`; // left pad
if (mm < 10) mm = `0${mm}`; if (hh < 10) {
if (ss < 10) ss = `0${ss}`; hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
if (duration !== null) return `${hh}:${mm}:${ss}`; return `${hh}:${mm}:${ss}`;
return false;
}, },
},
methods: { finishedTimeFormated() {
changeTime() { const timeAgo = gl.utils.getTimeago();
this.currentTime = new Date();
return timeAgo.format(this.finishedTime);
}, },
}, },
template: ` template: `
<td class="pipelines-time-ago"> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p
<span v-html="iconTimerSvg"></span> class="duration"
{{duration}} v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p> </p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i> <p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true" />
<time <time
ref="tooltip"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished'> :title="localTimeFinished">
{{timeStopped.words}} {{finishedTimeFormated}}
</time> </time>
</p> </p>
</td> </td>
......
import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
...@@ -161,15 +160,6 @@ export default { ...@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
/* eslint-disable no-underscore-dangle*/
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore { export default class PipelinesStore {
constructor() { constructor() {
this.state = {}; this.state = {};
...@@ -30,32 +27,4 @@ export default class PipelinesStore { ...@@ -30,32 +27,4 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo; this.state.pageInfo = paginationInfo;
} }
/**
* FIXME: Move this inside the component.
*
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
VueRealtimeListener(removeIntervals, startIntervals);
}
} }
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id');
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
......
export default (removeIntervals, startIntervals) => {
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
window.removeEventListener('onbeforeload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
window.addEventListener('onbeforeload', removeIntervals);
};
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
...@@ -166,6 +165,32 @@ export default { ...@@ -166,6 +165,32 @@ export default {
} }
return undefined; return undefined;
}, },
/**
* Timeago components expects a number
*
* @return {type} description
*/
pipelineDuration() {
if (this.pipeline.details && this.pipeline.details.duration) {
return this.pipeline.details.duration;
}
return 0;
},
/**
* Timeago component expects a String.
*
* @return {String}
*/
pipelineFinishedAt() {
if (this.pipeline.details && this.pipeline.details.finished_at) {
return this.pipeline.details.finished_at;
}
return '';
},
}, },
template: ` template: `
...@@ -192,7 +217,9 @@ export default { ...@@ -192,7 +217,9 @@ export default {
</div> </div>
</td> </td>
<time-ago :pipeline="pipeline"/> <time-ago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt" />
<td class="pipeline-actions"> <td class="pipeline-actions">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
......
...@@ -227,8 +227,8 @@ ...@@ -227,8 +227,8 @@
.award-control-icon-positive, .award-control-icon-positive,
.award-control-icon-super-positive { .award-control-icon-super-positive {
position: absolute; position: absolute;
left: 7px; left: 11px;
bottom: 9px; bottom: 7px;
opacity: 0; opacity: 0;
@include transition(opacity, transform); @include transition(opacity, transform);
} }
......
...@@ -195,7 +195,6 @@ ...@@ -195,7 +195,6 @@
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible; @include set-invisible;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
......
...@@ -61,11 +61,13 @@ ...@@ -61,11 +61,13 @@
.file-content { .file-content {
background: $white-light; background: $white-light;
&.image_file { &.image_file,
&.video {
background: $file-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
img { img,
video {
padding: 20px; padding: 20px;
max-width: 80%; max-width: 80%;
} }
......
...@@ -104,6 +104,24 @@ ...@@ -104,6 +104,24 @@
padding: 2px 7px; padding: 2px 7px;
} }
.value {
padding-right: 0;
}
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
.fa-close {
color: $gl-text-color-disabled;
}
&:hover .fa-close {
color: $gl-text-color-secondary;
}
}
.name { .name {
background-color: $filter-name-resting-color; background-color: $filter-name-resting-color;
color: $filter-name-text-color; color: $filter-name-text-color;
...@@ -112,7 +130,7 @@ ...@@ -112,7 +130,7 @@
text-transform: capitalize; text-transform: capitalize;
} }
.value { .value-container {
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $filter-value-text-color;
border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0;
...@@ -124,7 +142,7 @@ ...@@ -124,7 +142,7 @@
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
} }
.value { .value-container {
background-color: $filter-value-selected-color; background-color: $filter-value-selected-color;
} }
} }
......
...@@ -120,6 +120,10 @@ ...@@ -120,6 +120,10 @@
// Ensure that image does not exceed viewport // Ensure that image does not exceed viewport
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
} }
table {
@include markdown-table;
}
} }
.toolbar-group { .toolbar-group {
......
...@@ -12,6 +12,13 @@ ...@@ -12,6 +12,13 @@
max-width: $max_width; max-width: $max_width;
} }
/*
* Mixin for markdown tables
*/
@mixin markdown-table {
width: auto;
}
/* /*
* Base mixin for lists in GitLab * Base mixin for lists in GitLab
*/ */
......
...@@ -200,6 +200,7 @@ ...@@ -200,6 +200,7 @@
.header-content { .header-content {
flex: 1; flex: 1;
line-height: 1.8;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -110,11 +110,16 @@ ul.related-merge-requests > li { ...@@ -110,11 +110,16 @@ ul.related-merge-requests > li {
} }
} }
.merge-request-ci-status { .merge-request-ci-status,
.related-merge-requests {
.ci-status-link {
display: block;
margin-top: 3px;
margin-right: 5px;
}
svg { svg {
margin-right: 4px; display: block;
position: relative;
top: 1px;
} }
} }
......
...@@ -97,6 +97,10 @@ ul.notes { ...@@ -97,6 +97,10 @@ ul.notes {
padding-left: 1.3em; padding-left: 1.3em;
} }
} }
table {
@include markdown-table;
}
} }
} }
......
...@@ -614,6 +614,7 @@ pre.light-well { ...@@ -614,6 +614,7 @@ pre.light-well {
.controls { .controls {
margin-left: auto; margin-left: auto;
text-align: right;
} }
.ci-status-link { .ci-status-link {
......
...@@ -160,7 +160,6 @@ ...@@ -160,7 +160,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px;
position: relative; position: relative;
z-index: 2; z-index: 2;
......
...@@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list { ...@@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list {
padding: 5px 0; padding: 5px 0;
} }
} }
.wiki {
table {
@include markdown-table;
}
}
...@@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base ...@@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base
end end
end end
def respond_422
head :unprocessable_entity
end
def no_cache_headers def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache" response.headers["Pragma"] = "no-cache"
......
module MarkdownPreview
private
def render_markdown_preview(text, markdown_context = {})
render json: {
body: view_context.markdown(text, markdown_context),
references: {
users: preview_referenced_users(text)
}
}
end
def preview_referenced_users(text)
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
end
module RendersBlob
extend ActiveSupport::Concern
def render_blob_json(blob)
viewer =
if params[:viewer] == 'rich'
blob.rich_viewer
else
blob.simple_viewer
end
return render_404 unless viewer
render json: {
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
}
end
def override_max_blob_size(blob)
blob.override_max_size! if params[:override_max_size] == 'true'
end
end
...@@ -38,6 +38,7 @@ module ServiceParams ...@@ -38,6 +38,7 @@ module ServiceParams
:new_issue_url, :new_issue_url,
:notify, :notify,
:notify_only_broken_pipelines, :notify_only_broken_pipelines,
:notify_only_default_branch,
:password, :password,
:priority, :priority,
:project_key, :project_key,
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Projects::BlobController < Projects::ApplicationController class Projects::BlobController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include CreatesCommit include CreatesCommit
include RendersBlob
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path # Raised when given an invalid file path
...@@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController
end end
def show def show
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } override_max_blob_size(@blob)
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
respond_to do |format|
format.html do
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
render 'show'
end
format.json do
render_blob_json(@blob)
end
end
end end
def edit def edit
...@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
private private
def blob def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path)) @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
if @blob if @blob
@blob @blob
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play] before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace] before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project' layout 'project'
...@@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def retry def retry
return render_404 unless @build.retryable? return respond_422 unless @build.retryable?
build = Ci::Build.retry(@build, current_user) build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def play def play
return render_404 unless @build.playable? return respond_422 unless @build.playable?
build = @build.play(current_user) build = @build.play(current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def cancel def cancel
return respond_422 unless @build.cancelable?
@build.cancel @build.cancel
redirect_to build_path(@build) redirect_to build_path(@build)
end end
...@@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def erase def erase
@build.erase(erased_by: current_user) if @build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build), redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been successfully erased!" notice: "Build has been successfully erased!"
else
respond_422
end
end end
def raw def raw
......
...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
if @blob.lfs_pointer? && project.lfs_enabled? if @blob.valid_lfs_pointer?
send_lfs_object send_lfs_object
else else
send_git_blob @repository, @blob send_git_blob @repository, @blob
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
before_action :authorize_admin_project! before_action :authorize_admin_project!
layout "project_settings" layout "project_settings"
def show def show
@hooks = @project.hooks @hooks = @project.hooks
@hook = ProjectHook.new @hook = ProjectHook.new
......
...@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include RendersBlob
before_action :module_enabled before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
...@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def show def show
@note = @project.notes.new(noteable: @snippet) blob = @snippet.blob
@noteable = @snippet override_max_blob_size(blob)
@discussions = @snippet.discussions respond_to do |format|
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) format.html do
@note = @project.notes.new(noteable: @snippet)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end end
def destroy def destroy
......
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController
) )
end end
def preview_markdown def git_access
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: ext.users.map(&:username)
}
}
end end
def git_access def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
render_markdown_preview(params[:text], context)
end end
private private
...@@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController
} }
end end
def preview_markdown
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text),
references: {
users: ext.users.map(&:username)
}
}
end
def refs def refs
branches = BranchesFinder.new(@repository, params).execute.map(&:name) branches = BranchesFinder.new(@repository, params).execute.map(&:name)
...@@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json render json: options.to_json
end end
def preview_markdown
render_markdown_preview(params[:text])
end
private private
# Render project landing depending of which features are available # Render project landing depending of which features are available
......
...@@ -2,6 +2,8 @@ class SnippetsController < ApplicationController ...@@ -2,6 +2,8 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
...@@ -59,6 +61,18 @@ class SnippetsController < ApplicationController ...@@ -59,6 +61,18 @@ class SnippetsController < ApplicationController
end end
def show def show
blob = @snippet.blob
override_max_blob_size(blob)
respond_to do |format|
format.html do
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end end
def destroy def destroy
...@@ -77,6 +91,10 @@ class SnippetsController < ApplicationController ...@@ -77,6 +91,10 @@ class SnippetsController < ApplicationController
) )
end end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
protected protected
def snippet def snippet
......
if Rails.env.test?
class UnicornTestController < ActionController::Base
def pid
render plain: Process.pid.to_s
end
def kill
Process.kill(params[:signal], Process.pid)
render plain: 'Bye!'
end
end
end
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# current_user - which user use # current_user - which user use
# params: # params:
# scope: 'created-by-me' or 'assigned-to-me' or 'all' # scope: 'created-by-me' or 'assigned-to-me' or 'all'
# state: 'open' or 'closed' or 'all' # state: 'open', 'closed', 'merged', or 'all'
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_title: string # milestone_title: string
......
...@@ -196,38 +196,6 @@ module ApplicationHelper ...@@ -196,38 +196,6 @@ module ApplicationHelper
end end
end end
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown(file_content))
elsif asciidoc?(file_name)
asciidoc(file_content)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
file_content
end
else
other_markup(file_name, file_content)
end
rescue RuntimeError
simple_format(file_content)
end
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
def promo_host def promo_host
'about.gitlab.com' 'about.gitlab.com'
end end
......
...@@ -52,7 +52,7 @@ module BlobHelper ...@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer? elsif blob.valid_lfs_pointer?
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
...@@ -95,7 +95,7 @@ module BlobHelper ...@@ -95,7 +95,7 @@ module BlobHelper
end end
def can_modify_blob?(blob, project = @project, ref = @ref) def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref) !blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
end end
def leave_edit_message def leave_edit_message
...@@ -118,28 +118,23 @@ module BlobHelper ...@@ -118,28 +118,23 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw") icon("#{file_type_icon_class('file', mode, name)} fw")
end end
def blob_text_viewable?(blob) def blob_raw_url
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw? if @snippet
end if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
def blob_rendered_as_text?(blob) else
blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text' raw_snippet_path(@snippet)
end end
elsif @blob
def blob_size(blob) namespace_project_raw_path(@project.namespace, @project, @id)
if blob.lfs_pointer?
blob.lfs_size
else
blob.size
end end
end end
# SVGs can contain malicious JavaScript; only include whitelisted # SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete # elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements. # and may omit some elements.
def sanitize_svg(blob) def sanitize_svg_data(data)
blob.data = Gitlab::Sanitizers::SVG.clean(blob.data) Gitlab::Sanitizers::SVG.clean(data)
blob
end end
# If we blindly set the 'real' content type when serving a Git blob we # If we blindly set the 'real' content type when serving a Git blob we
...@@ -221,13 +216,52 @@ module BlobHelper ...@@ -221,13 +216,52 @@ module BlobHelper
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end end
def copy_blob_content_button(blob) def copy_blob_source_button(blob)
return if markup?(blob.name) return unless blob.rendered_as_text?(ignore_errors: false)
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
end end
def open_raw_file_button(path) def open_raw_blob_button(blob)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } if blob.raw_binary?
icon = icon('download')
title = 'Download'
else
icon = icon('file-code-o')
title = 'Open raw'
end
link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def blob_render_error_reason(viewer)
case viewer.render_error
when :too_large
max_size =
if viewer.absolutely_too_large?
viewer.absolute_max_size
elsif viewer.too_large?
viewer.max_size
end
"it is larger than #{number_to_human_size(max_size)}"
when :server_side_but_stored_in_lfs
"it is stored in LFS"
end
end
def blob_render_error_options(viewer)
options = []
if viewer.render_error == :too_large && viewer.can_override_max_size?
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
end
if viewer.rich? && viewer.blob.rendered_as_text?
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end
options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
options
end end
end end
...@@ -10,11 +10,12 @@ module EventsHelper ...@@ -10,11 +10,12 @@ module EventsHelper
'deleted' => 'icon_trash_o' 'deleted' => 'icon_trash_o'
}.freeze }.freeze
def link_to_author(event) def link_to_author(event, self_added: false)
author = event.author author = event.author
if author if author
link_to author.name, user_path(author.username), title: author.name name = self_added ? 'You' : author.name
link_to name, user_path(author.username), title: name
else else
event.author_name event.author_name
end end
......
require 'nokogiri' require 'nokogiri'
module GitlabMarkdownHelper module MarkupHelper
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
# Use this in places where you would normally use link_to(gfm(...), ...). # Use this in places where you would normally use link_to(gfm(...), ...).
# #
# It solves a problem occurring with nested links (i.e. # It solves a problem occurring with nested links (i.e.
...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper ...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper
# explicitly produce the correct linking behavior (i.e. # explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return '' if body.blank?
context = { context = {
project: @project, project: @project,
...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper ...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper
fragment.to_html.html_safe fragment.to_html.html_safe
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present?
end
def markdown(text, context = {}) def markdown(text, context = {})
return "" unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
html = markdown_unsafe(text, context)
html = Banzai.render(text, context) prepare_for_rendering(html, context)
banzai_postprocess(html, context)
end end
def markdown_field(object, field) def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display) object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present? return '' unless object.present?
html = Banzai.render_field(object, field) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field)) prepare_for_rendering(html, object.banzai_render_context(field))
end end
def asciidoc(text) def markup(file_name, text, context = {})
Gitlab::Asciidoc.render( context[:project] ||= @project
text, html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
project: @project, prepare_for_rendering(html, context)
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end end
def other_markup(file_name, text) def render_wiki_content(wiki_page)
Gitlab::OtherMarkup.render( text = wiki_page.content
file_name, return '' unless text.present?
text,
project: @project, context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
current_user: (current_user if defined?(current_user)),
html =
case wiki_page.format
when :markdown
markdown_unsafe(text, context)
when :asciidoc
asciidoc_unsafe(text)
else
wiki_page.formatted_content.html_safe
end
# RelativeLinkFilter prepare_for_rendering(html, context)
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line def markup_unsafe(file_name, text, context = {})
# as Markdown. HTML tags in the parsed output are not counted toward the return '' unless text.present?
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present? if gitlab_markdown?(file_name)
end markdown_unsafe(text, context)
elsif asciidoc?(file_name)
def render_wiki_content(wiki_page) asciidoc_unsafe(text)
case wiki_page.format elsif plain?(file_name)
when :markdown content_tag :pre, class: 'plain-readme' do
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) text
when :asciidoc end
asciidoc(wiki_page.content)
else else
wiki_page.formatted_content.html_safe other_markup_unsafe(file_name, text)
end end
rescue RuntimeError
simple_format(text)
end end
# Returns the text necessary to reference `entity` across projects # Returns the text necessary to reference `entity` across projects
...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper ...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper
end end
def markdown_toolbar_button(options = {}) def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" }) data = options[:data].merge({ container: 'body' })
content_tag :button, content_tag :button,
type: "button", type: 'button',
class: "toolbar-btn js-md has-tooltip hidden-xs", class: 'toolbar-btn js-md has-tooltip hidden-xs',
tabindex: -1, tabindex: -1,
data: data, data: data,
title: options[:title], title: options[:title],
...@@ -195,17 +213,35 @@ module GitlabMarkdownHelper ...@@ -195,17 +213,35 @@ module GitlabMarkdownHelper
end end
end end
# Calls Banzai.post_process with some common context options def markdown_unsafe(text, context = {})
def banzai_postprocess(html, context) Banzai.render(text, context)
end
def asciidoc_unsafe(text)
Gitlab::Asciidoc.render(text)
end
def other_markup_unsafe(file_name, text)
Gitlab::OtherMarkup.render(file_name, text)
end
def prepare_for_rendering(html, context = {})
return '' unless html.present?
context.merge!( context.merge!(
current_user: (current_user if defined?(current_user)), current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter # RelativeLinkFilter
requested_path: @path, commit: @commit,
project_wiki: @project_wiki, project_wiki: @project_wiki,
ref: @ref ref: @ref,
requested_path: @path
) )
Banzai.post_process(html, context) html = Banzai.post_process(html, context)
Hamlit::RailsHelpers.preserve(html)
end end
extend self
end end
...@@ -56,11 +56,12 @@ module MergeRequestsHelper ...@@ -56,11 +56,12 @@ module MergeRequestsHelper
end end
def issues_sentence(issues) def issues_sentence(issues)
# Sorting based on the `#123` or `group/project#123` reference will sort # Issuable sorter will sort local issues, then issues from the same
# local issues first. # namespace, then all other issues.
issues.map do |issue| issues = Gitlab::IssuableSorter.sort(@project, issues).map do |issue|
issue.to_reference(@project) issue.to_reference(@project)
end.sort.to_sentence end
issues.to_sentence
end end
def mr_closes_issues def mr_closes_issues
......
...@@ -160,12 +160,17 @@ module ProjectsHelper ...@@ -160,12 +160,17 @@ module ProjectsHelper
end end
def project_list_cache_key(project) def project_list_cache_key(project)
key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3'] key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key key
end end
def load_pipeline_status(projects)
Gitlab::Cache::Ci::ProjectPipelineStatus.
load_in_batch_for_projects(projects)
end
private private
def repo_children_classes(field) def repo_children_classes(field)
......
...@@ -13,8 +13,8 @@ module ServicesHelper ...@@ -13,8 +13,8 @@ module ServicesHelper
"Event will be triggered when a confidential issue is created/updated/closed" "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events" when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged" "Event will be triggered when a merge request is created/updated/merged"
when "build", "build_events" when "pipeline", "pipeline_events"
"Event will be triggered when a build status changes" "Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events" when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events" when "commit", "commit_events"
......
...@@ -13,13 +13,13 @@ module TodosHelper ...@@ -13,13 +13,13 @@ module TodosHelper
def todo_action_name(todo) def todo_action_name(todo)
case todo.action case todo.action
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on"
when Todo::BUILD_FAILED then 'The build failed for' when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for' when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for' when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for"
when Todo::UNMERGEABLE then 'Could not merge' when Todo::UNMERGEABLE then 'Could not merge'
when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on' when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on"
end end
end end
...@@ -148,6 +148,10 @@ module TodosHelper ...@@ -148,6 +148,10 @@ module TodosHelper
private private
def todo_action_subject(todo)
todo.self_added? ? 'yourself' : 'you'
end
def show_todo_state?(todo) def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end end
......
...@@ -12,10 +12,6 @@ module TreeHelper ...@@ -12,10 +12,6 @@ module TreeHelper
tree.html_safe tree.html_safe
end end
def render_readme(readme)
render_markup(readme.name, readme.data)
end
# Return an image icon depending on the file type and mode # Return an image icon depending on the file type and mode
# #
# type - String type of the tree item; either 'folder' or 'file' # type - String type of the tree item; either 'folder' or 'file'
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
helper ApplicationHelper helper ApplicationHelper
helper GitlabMarkdownHelper helper MarkupHelper
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?
......
...@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :uuid, presence: true
validates :session_expire_delay, validates :session_expire_delay,
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
...@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
...@@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base
private private
def ensure_uuid!
return if uuid?
self.uuid = SecureRandom.uuid
end
def check_repository_storages def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
......
...@@ -3,8 +3,42 @@ class Blob < SimpleDelegator ...@@ -3,8 +3,42 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
# The maximum size of an SVG that can be displayed. MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
MAXIMUM_SVG_SIZE = 2.megabytes
# Finding a viewer for a blob happens based only on extension and whether the
# blob is binary or text, which means 1 blob should only be matched by 1 viewer,
# and the order of these viewers doesn't really matter.
#
# However, when the blob is an LFS pointer, we cannot know for sure whether the
# file being pointed to is binary or text. In this case, we match only on
# extension, preferring binary viewers over text ones if both exist, since the
# large files referred to in "Large File Storage" are much more likely to be
# binary than text.
#
# `.stl` files, for example, exist in both binary and text forms, and are
# handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
# type. LFS pointers to `.stl` files are assumed to always be the binary kind,
# and use the `BinarySTL` viewer.
RICH_VIEWERS = [
BlobViewer::Markup,
BlobViewer::Notebook,
BlobViewer::SVG,
BlobViewer::Image,
BlobViewer::Sketch,
BlobViewer::Video,
BlobViewer::PDF,
BlobViewer::BinarySTL,
BlobViewer::TextSTL,
].freeze
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
attr_reader :project
# Wrap a Gitlab::Git::Blob object, or return nil when given nil # Wrap a Gitlab::Git::Blob object, or return nil when given nil
# #
...@@ -16,10 +50,16 @@ class Blob < SimpleDelegator ...@@ -16,10 +50,16 @@ class Blob < SimpleDelegator
# #
# blob = Blob.decorate(nil) # blob = Blob.decorate(nil)
# puts "truthy" if blob # No output # puts "truthy" if blob # No output
def self.decorate(blob) def self.decorate(blob, project = nil)
return if blob.nil? return if blob.nil?
new(blob) new(blob, project)
end
def initialize(blob, project = nil)
@project = project
super(blob)
end end
# Returns the data of the blob. # Returns the data of the blob.
...@@ -35,82 +75,107 @@ class Blob < SimpleDelegator ...@@ -35,82 +75,107 @@ class Blob < SimpleDelegator
end end
def no_highlighting? def no_highlighting?
size && size > 1.megabyte size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end end
def only_display_raw? def too_large?
size && truncated? size && truncated?
end end
# Returns the size of the file that this blob represents. If this blob is an
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
# the size of the blob itself.
def raw_size
if valid_lfs_pointer?
lfs_size
else
size
end
end
# Returns whether the file that this blob represents is binary. If this blob is
# an LFS pointer, we assume the file stored in LFS is binary, unless a
# text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself.
def raw_binary?
if valid_lfs_pointer?
if rich_viewer
rich_viewer.binary?
else
true
end
else
binary?
end
end
def extension def extension
extname.downcase.delete('.') @extension ||= extname.downcase.delete('.')
end end
def svg? def video?
text? && language && language.name == 'SVG' UploaderHelper::VIDEO_EXT.include?(extension)
end end
def pdf? def readable_text?
extension == 'pdf' text? && !valid_lfs_pointer? && !too_large?
end end
def ipython_notebook? def valid_lfs_pointer?
text? && language&.name == 'Jupyter Notebook' lfs_pointer? && project&.lfs_enabled?
end end
def sketch? def invalid_lfs_pointer?
binary? && extension == 'sketch' lfs_pointer? && !project&.lfs_enabled?
end end
def stl? def simple_viewer
extension == 'stl' @simple_viewer ||= simple_viewer_class.new(self)
end end
def markup? def rich_viewer
text? && Gitlab::MarkupHelper.markup?(name) return @rich_viewer if defined?(@rich_viewer)
@rich_viewer = rich_viewer_class&.new(self)
end end
def size_within_svg_limits? def rendered_as_text?(ignore_errors: true)
size <= MAXIMUM_SVG_SIZE simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end end
def video? def show_viewer_switcher?
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) rendered_as_text? && rich_viewer
end end
def to_partial_path(project) def override_max_size!
if lfs_pointer? simple_viewer&.override_max_size = true
if project.lfs_enabled? rich_viewer&.override_max_size = true
'download' end
else
'text' private
end
elsif image? def simple_viewer_class
'image' if empty?
elsif svg? BlobViewer::Empty
'svg' elsif raw_binary?
elsif pdf? BlobViewer::Download
'pdf' else # text
elsif ipython_notebook? BlobViewer::Text
'notebook'
elsif sketch?
'sketch'
elsif stl?
'stl'
elsif markup?
if only_display_raw?
'too_large'
else
'markup'
end
elsif text?
if only_display_raw?
'too_large'
else
'text'
end
else
'download'
end end
end end
def rich_viewer_class
return if invalid_lfs_pointer? || empty?
classes =
if valid_lfs_pointer?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
else # text
TEXT_VIEWERS
end
classes.find { |viewer_class| viewer_class.can_render?(self) }
end
end end
module BlobViewer
class Base
class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
attr_reader :blob
attr_accessor :override_max_size
def initialize(blob)
@blob = blob
end
def self.partial_path
"projects/blob/viewers/#{partial_name}"
end
def self.rich?
type == :rich
end
def self.simple?
type == :simple
end
def self.client_side?
client_side
end
def self.server_side?
!client_side?
end
def self.binary?
binary
end
def self.text?
!binary?
end
def self.can_render?(blob)
!extensions || extensions.include?(blob.extension)
end
def too_large?
blob.raw_size > max_size
end
def absolutely_too_large?
blob.raw_size > absolute_max_size
end
def can_override_max_size?
too_large? && !absolutely_too_large?
end
# This method is used on the server side to check whether we can attempt to
# render the blob at all. Human-readable error messages are found in the
# `BlobHelper#blob_render_error_reason` helper.
#
# This method does not and should not load the entire blob contents into
# memory, and should not be overridden to do so in order to validate the
# format of the blob.
#
# Prefer to implement a client-side viewer, where the JS component loads the
# binary from `blob_raw_url` and does its own format validation and error
# rendering, especially for potentially large binary formats.
def render_error
return @render_error if defined?(@render_error)
@render_error =
if server_side_but_stored_in_lfs?
# Files stored in LFS can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX.
:server_side_but_stored_in_lfs
elsif override_max_size ? absolutely_too_large? : too_large?
:too_large
end
end
def prepare!
if server_side? && blob.project
blob.load_all_data!(blob.project.repository)
end
end
private
def server_side_but_stored_in_lfs?
server_side? && blob.valid_lfs_pointer?
end
end
end
module BlobViewer
class BinarySTL < Base
include Rich
include ClientSide
self.partial_name = 'stl'
self.extensions = %w(stl)
self.binary = true
end
end
module BlobViewer
module ClientSide
extend ActiveSupport::Concern
included do
self.client_side = true
self.max_size = 10.megabytes
self.absolute_max_size = 50.megabytes
end
end
end
module BlobViewer
class Download < Base
include Simple
# We treat the Download viewer as if it renders the content client-side,
# so that it doesn't attempt to load the entire blob contents and is
# rendered synchronously instead of loaded asynchronously.
include ClientSide
self.partial_name = 'download'
self.binary = true
# We can always render the Download viewer, even if the blob is in LFS or too large.
def render_error
nil
end
end
end
module BlobViewer
class Empty < Base
include Simple
include ServerSide
self.partial_name = 'empty'
self.binary = true
end
end
module BlobViewer
class Image < Base
include Rich
include ClientSide
self.partial_name = 'image'
self.extensions = UploaderHelper::IMAGE_EXT
self.binary = true
self.switcher_icon = 'picture-o'
self.switcher_title = 'image'
end
end
module BlobViewer
class Markup < Base
include Rich
include ServerSide
self.partial_name = 'markup'
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
self.binary = false
end
end
module BlobViewer
class Notebook < Base
include Rich
include ClientSide
self.partial_name = 'notebook'
self.extensions = %w(ipynb)
self.binary = false
self.switcher_icon = 'file-text-o'
self.switcher_title = 'notebook'
end
end
module BlobViewer
class PDF < Base
include Rich
include ClientSide
self.partial_name = 'pdf'
self.extensions = %w(pdf)
self.binary = true
self.switcher_icon = 'file-pdf-o'
self.switcher_title = 'PDF'
end
end
module BlobViewer
module Rich
extend ActiveSupport::Concern
included do
self.type = :rich
self.switcher_icon = 'file-text-o'
self.switcher_title = 'rendered file'
end
end
end
module BlobViewer
module ServerSide
extend ActiveSupport::Concern
included do
self.client_side = false
self.max_size = 2.megabytes
self.absolute_max_size = 5.megabytes
end
end
end
module BlobViewer
module Simple
extend ActiveSupport::Concern
included do
self.type = :simple
self.switcher_icon = 'code'
self.switcher_title = 'source'
end
end
end
module BlobViewer
class Sketch < Base
include Rich
include ClientSide
self.partial_name = 'sketch'
self.extensions = %w(sketch)
self.binary = true
self.switcher_icon = 'file-image-o'
self.switcher_title = 'preview'
end
end
module BlobViewer
class SVG < Base
include Rich
include ServerSide
self.partial_name = 'svg'
self.extensions = %w(svg)
self.binary = false
self.switcher_icon = 'picture-o'
self.switcher_title = 'image'
end
end
module BlobViewer
class Text < Base
include Simple
include ServerSide
self.partial_name = 'text'
self.binary = false
self.max_size = 1.megabyte
self.absolute_max_size = 10.megabytes
end
end
module BlobViewer
class TextSTL < BinarySTL
self.binary = false
end
end
module BlobViewer
class Video < Base
include Rich
include ClientSide
self.partial_name = 'video'
self.extensions = UploaderHelper::VIDEO_EXT
self.binary = true
self.switcher_icon = 'film'
self.switcher_title = 'video'
end
end
...@@ -316,7 +316,7 @@ class Commit ...@@ -316,7 +316,7 @@ class Commit
def uri_type(path) def uri_type(path)
entry = @raw.tree.path(path) entry = @raw.tree.path(path)
if entry[:type] == :blob if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name])) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob blob.image? || blob.video? ? :raw : :blob
else else
entry[:type] entry[:type]
......
...@@ -120,7 +120,9 @@ module CacheMarkdownField ...@@ -120,7 +120,9 @@ module CacheMarkdownField
attrs attrs
end end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache? # Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
......
...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true
......
...@@ -34,6 +34,7 @@ class Label < ActiveRecord::Base ...@@ -34,6 +34,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) } scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) } scope :with_title, ->(title) { where(title: title) }
scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
def self.prioritized(project) def self.prioritized(project)
joins(:priorities) joins(:priorities)
......
...@@ -154,6 +154,11 @@ class Member < ActiveRecord::Base ...@@ -154,6 +154,11 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil) def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present? return [] unless users.present?
# Collect all user ids into separate array
# so we can use single sql query to get user objects
user_ids = users.select { |user| user =~ /\A\d+\Z/ }
users = users - user_ids + User.where(id: user_ids)
self.transaction do self.transaction do
users.map do |user| users.map do |user|
add_user( add_user(
......
...@@ -107,7 +107,8 @@ module Network ...@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0) def find_commits(skip = 0)
opts = { opts = {
max_count: self.class.max_count, max_count: self.class.max_count,
skip: skip skip: skip,
order: :date
} }
opts[:ref] = @commit.id if @filter_ref opts[:ref] = @commit.id if @filter_ref
......
...@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base ...@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_writer :pipeline_status
alias_attribute :title, :name alias_attribute :title, :name
...@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base ...@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base
end end
end end
# Lazy loading of the `pipeline_status` attribute
def pipeline_status def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end end
......
...@@ -22,7 +22,7 @@ class ChatNotificationService < Service ...@@ -22,7 +22,7 @@ class ChatNotificationService < Service
end end
def can_test? def can_test?
super && valid? valid?
end end
def self.supported_events def self.supported_events
......
...@@ -17,9 +17,9 @@ class Repository ...@@ -17,9 +17,9 @@ class Repository
# same name. The cache key used by those methods must also match method's # same name. The cache key used by those methods must also match method's
# name. # name.
# #
# For example, for entry `:readme` there's a method called `readme` which # For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `readme` cache key. # stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme contribution_guide CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze tag_count avatar exists? empty? root_ref).freeze
...@@ -28,7 +28,7 @@ class Repository ...@@ -28,7 +28,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches. # the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = { METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme, readme: :rendered_readme,
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key), license: %i(license_blob license_key),
contributing: :contribution_guide, contributing: :contribution_guide,
...@@ -450,7 +450,7 @@ class Repository ...@@ -450,7 +450,7 @@ class Repository
def blob_at(sha, path) def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha) unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
end end
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
...@@ -527,7 +527,11 @@ class Repository ...@@ -527,7 +527,11 @@ class Repository
head.readme head.readme
end end
end end
cache_method :readme
def rendered_readme
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
end
cache_method :rendered_readme
def contribution_guide def contribution_guide
file_on_head(:contributing) file_on_head(:contributing)
...@@ -957,15 +961,13 @@ class Repository ...@@ -957,15 +961,13 @@ class Repository
end end
def is_ancestor?(ancestor_id, descendant_id) def is_ancestor?(ancestor_id, descendant_id)
# NOTE: This feature is intentionally disabled until Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
# https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved if is_enabled
# Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| raw_repository.is_ancestor?(ancestor_id, descendant_id)
# if is_enabled else
# raw_repository.is_ancestor?(ancestor_id, descendant_id) merge_base_commit(ancestor_id, descendant_id) == ancestor_id
# else end
merge_base_commit(ancestor_id, descendant_id) == ancestor_id end
# end
# end
end end
def empty_repo? def empty_repo?
......
...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.template? } validates :project_id, presence: true, unless: proc { |service| service.template? }
validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
...@@ -131,7 +132,7 @@ class Service < ActiveRecord::Base ...@@ -131,7 +132,7 @@ class Service < ActiveRecord::Base
end end
def can_test? def can_test?
!project.empty_repo? true
end end
# reason why service cannot be tested # reason why service cannot be tested
......
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Linguist::BlobHelper
include CacheMarkdownField include CacheMarkdownField
include Noteable include Noteable
include Participable include Participable
...@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base ...@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base
] ]
end end
def data def blob
content @blob ||= Blob.decorate(SnippetBlob.new(self), nil)
end end
def hook_attrs def hook_attrs
attributes attributes
end end
def size
0
end
def file_name def file_name
super.to_s super.to_s
end end
# alias for compatibility with blobs and highlighting
def path
file_name
end
def name
file_name
end
def sanitized_file_name def sanitized_file_name
file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
end end
def mode
nil
end
def visibility_level_field def visibility_level_field
:visibility_level :visibility_level
end end
def no_highlighting?
content.lines.count > 1000
end
def notes_with_associations def notes_with_associations
notes.includes(:author) notes.includes(:author)
end end
......
class SnippetBlob
include Linguist::BlobHelper
attr_reader :snippet
def initialize(snippet)
@snippet = snippet
end
delegate :id, to: :snippet
def name
snippet.file_name
end
alias_method :path, :name
def size
data.bytesize
end
def data
snippet.content
end
def rendered_markup
return unless Gitlab::MarkupHelper.gitlab_markdown?(name)
Banzai.render_field(snippet, :content)
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
false
end
def lfs_oid
nil
end
def lfs_size
nil
end
def truncated?
false
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment