Groundwork for merging CI into CE

This commit is contained in:
Douwe Maan
2015-08-25 18:42:46 -07:00
parent e449426a4e
commit 046b283127
573 changed files with 23499 additions and 3644 deletions
+298
View File
@@ -0,0 +1,298 @@
v7.14.0 (unreleased)
- Truncate commit messages after subject line in table
- Adjust CI config to support Docker executors
- Added Application Settings
- Randomize test database for CI tests
- Make YAML validation stricter
- Use avatars received from GitLab
- Refactor GitLab API usage to use either access_token or private_token depending on what was specified during login
- Allow to use access_token for API requests
- Fix project API listing returning empty list when first projects are not added to CI
- Allow to define variables from YAML
- Added support for CI skipped status
- Fix broken yaml error saving
- Add committed_at to commits to properly order last commit (the force push issue)
- Rename type(s) to stage(s)
- Fix navigation icons
- Add missing stage when doing retry
- Require variable keys to be not-empty and unique
- Fix variable saving issue
- Display variable saving errors in variables page not the project's
- Added Build Triggers API
v7.13.1
- Fix: user could steal specific runner
- Fix: don't send notifications for jobs with allow_failure set
- Fix invalid link to doc.gitlab.com
v7.13.0
- Fix inline edit runner-description
- Allow to specify image and services in yml that can be used with docker
- Fix: No runner notification can see managers only
- Fix service testing for slack
- Ability to cancel all builds in commit at once
- Disable colors in rake tasks automatically (if IO is not a TTY)
- Implemented "rake env:info". Rake task to receive system information
- Fix coverage calculation on commit page
- Enhance YAML validation
- Redirect back after authorization
- Change favicon
- Refactoring: Get rid of private_token usage in the frontend.
- Allow to specify allow_failure for job
- Build traces is stored in the file instead of database
- Make the builds path configurable
- Disable link to runner if it's not assigned to specific project
- Store all secrets in config/secrets.yml
- Encrypt variables
- Allow to specify flexible list of types in yaml
v7.12.2
- Revert: Runner without tag should pick builds without tag only
v7.12.1
- Runner without tag should pick builds without tag only
- Explicit error in the GitLab when commit not found.
- Fix: lint with relative subpath
- Update webhook example
- Improved Lint stability
- Add warning when .gitlab-ci.yml not found
- Improved validation for .gitlab-ci.yml
- Fix list of branches in only section
- Fix "Status Badge" button
v7.12.0
- Endless scroll on the dashboard
- Add notification if there are no runners
- Fix pagination on dashboard
- Remove ID column from runners list in the admin area
- Increase default timeout for builds to 60 minutes
- Using .gitlab-ci.yml file instead of jobs
- Link to the runner from the build page for admin user
- Ability to set secret variables for runner
- Dont retry build when push same commit in same ref twice
- Admin area: show amount of runners with last contact less than a minute ago
- Fix re-adding project with the same name but different gitlab_id
- Implementation of Lint (.gitlab-ci.yml validation tool)
- Updated rails to 4.1.11
- API fix: project create call
- Link to web-editor with .gitlab-ci.yml
- Updated examples in the documentation
v7.11.0
- Deploy Jobs API calls
- Projects search on dashboard page
- Improved runners page
- Running and Pending tabs on admin builds page
- Fix [ci skip] tag, so you can skip CI triggering now
- Add HipChat notifications
- Clean up project advanced settings.
- Add a GitLab project path parameter to the project API
- Remove projects IDs from dashboard
- UI fix: Remove page headers from the admin area
- Improve Email templates
- Add backup/restore utility
- Coordinator stores information(version, platform, revision, etc.) about runners.
- Fixed pagination on dashboard
- Public accessible build and commit pages of public projects
- Fix vulnerability in the API when MySQL is used
v7.10.1
- Fix failing migration when update to 7.10 from 7.8 and older versions
sidekiq_wirker_fix
- added sidekiq.yml
- integrated in script/background_jobs
v7.10.0
- Projects sorting by last commit date
- Add project search at runner page
- Fix GitLab and CI projects collision
- Events for admin
- Events per projects
- Search for runners in admin area
- UI improvements: created separated admin section, removed useless project show page
- Runners sorting in admin area (by id)
- Remove protected_attributes gem
- Skip commit creation if there is no appropriate job
v7.9.3
- Contains no changes
- Developers can cancel and retry jobs
v7.9.2
- [Security] Already existing projects should not be served by shared runners
- Ability to run deploy job without test jobs (every push will trigger deploy job)
v7.9.1
- [Security] Adding explicit is_shared parameter to runner
- [Security] By default new projects are not served by shared runners
v7.9.0
- Reset user session if token is invalid
- Runner delete api endpoint
- Fix bug about showing edit button on commit page if user does not have permissions
- Allow to pass description and tag list during Runner's registration
- Added api for project jobs
- Implementation of deploy jobs after all parallel jobs(tests).
- Add scroll up/down buttons for better mobile experience with large build traces
- Add runner last contact (Kamil Trzciński)
- Allow to pause runners - when paused runner will not receive any new build (Kamil Trzciński)
- Add brakeman (security scanner for Ruby on Rails)
- Changed a color of the canceled builds
- Fix of show the same commits in different branches
v7.8.2
- Fix the broken build failed email
- Notify only pusher instead of commiter
v7.8.0
- Fix OAuth login with GitLab installed in relative URL
- GitLab CI has same version as GitLab since now
- Allow to pass description and tag list during Runner's registration (Kamil Trzciński)
- Update documentation (API, Install, Update)
- Skip refs field supports for wildcard branch name (ex. feature/*)
- Migrate E-mail notification to Services menu (Kamil Trzciński)
- Added Slack notifications (Kamil Trzciński)
- Disable turbolink on links pointing out to GitLab server
- Add test coverage parsing example for pytest-cov
- Upgrade raindrops gem
v5.4.2
- Fix exposure of project token via build data
v5.4.1
- Fix 500 if on builds page if build has no job
- Truncate project token from build trace
- Allow users with access to project see build trace
v5.4.0 (Requires GitLab 7.7)
- Fixed 500 error for badge if build is pending
- Non-admin users can now register specific runners for their projects
- Project specific runners page which users can access
- Remove progress output from schedule_builds cron job
- Fix schedule_builds rake task
- Fix test webhook button
- Job can be branch specific or tag specific or both
- Shared runners builds projects which are not assigned to specific ones
- Job can be runner specific through tags
- Runner have tags
- Move job settings to separate page
- Add authorization level managing projects
- OAuth authentication via GitLab.
v5.3
- Remove annoying 'Done' message from schedule_builds cron job
- Fix a style issue with the navbar
- Skip CSRF check on the project's build page
- Fix showing wrong build script on admin projects page
- Add branch and commit message to build result emails
v5.2
- Improve performance by adding new indicies
- Separate Commit logic from Build logic in prep for Parallel Builds
- Parallel builds
- You can have multiple build scripts per project
v5.1
- Registration token and runner token are named differently
- Redirect to previous page after sign-in
- Dont show archived projects
- Add support for skip branches from build
- Add coverage parsing feature
- Update rails to 4.0.10
- Look for a REVISION file before running `git log`
- All builds page for admin
v5.0.1
- Update rails to 4.0.5
v5.0.0
- Set build timeout in minutes
- Web Hooks for builds
- Nprogress bar
- Remove extra spaces in build script
- Requires runner v5
* All script commands executed as one file
* Cancel button works correctly now
* Runner stability increased
* Timeout applies to build now instead of line of script
v4.3.0
- Refactor build js
- Redirect to build page with sha + bid if build id is not provided
- Update rails to 4.0.3
- Restyle project settings page
- Improve help page
- Replaced puma with unicorn
- Improved init.d script
- Add submodule init to default build script for new projects
v4.2.0
- Build duration chart
- Bootstrap 3 with responsive UI
- Improved init.d script
- Refactoring
- Changed http codes for POST /projects/:id/build action
- Turbolinks
v4.1.0
- Rails 4
- Click on build branch to see other builds for this branch
- Email notifications (Jeroen Knoops)
v4.0.0
- Shared runners (no need to add runner to every project)
- Admin area (only available for GitLab admins)
- Hide all runners management into admin area
- Use http cloning for builds instead of deploy keys
- Allow choose between git clone and git fetch when get code for build
- Make build timeout actually works
- Requires GitLab 6.3 or higher
- GitLab CI settings go to GitLab project via api on creation
v3.2.0
- Limit visibility of projects by gitlab authorized projects
- Use one page for both gitlab and gitlab-ci projects
v3.1.0
- Login with both username, email or LDAP credentials (if GitLab 6.0+)
- Retry build button functionality
- UI fixes for resolution 1366px and lower
- Fix gravatar ssl warning
v3.0.0
- Build running functionality extracted in gitlab-ci-runner
- Added API for runners and builds
- Redesigned application
- Added charts
- Use GitLab auth
- Add projects via UI with few clicks
v2.2.0
- replaced unicorn with puma
- replaced grit with rugged
- Runner.rb more transactional safe now
- updated rails to 3.2.13
- updated devise to 2.2
- fixed issue when build left in running status if exception triggered
- rescue build timeout correctly
- badge helper with markdown & html
- increased test coverage to 85%
v2.1.0
- Removed horizontal scroll for build trace
- new status badges
- better encode
- added several CI_* env variables
v2.0.0
- Replace resque with sidekiq
- Run only one build at time per project
- Added whenever for schedule jobs
v1.2.0
- Added Github web hook support
- Added build schedule
v1.1.0
- Added JSON response for builds status
- Compatible with GitLab v4.0.0
+115 -79
View File
@@ -1,6 +1,14 @@
source "https://rubygems.org"
gem 'rails', '4.1.11'
def darwin_only(require_as)
RUBY_PLATFORM.include?('darwin') && require_as
end
def linux_only(require_as)
RUBY_PLATFORM.include?('linux') && require_as
end
gem 'rails', '4.1.12'
# Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
@@ -10,28 +18,28 @@ gem 'sprockets', '~> 2.12.3'
gem "default_value_for", "~> 3.0.0"
# Supported DBs
gem "mysql2", group: :mysql
gem "pg", group: :postgres
gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
gem "devise", '3.2.4'
gem "devise-async", '0.9.0'
gem "devise", '~> 3.2.4'
gem "devise-async", '~> 0.9.0'
gem 'omniauth', "~> 1.2.2"
gem 'omniauth-google-oauth2'
gem 'omniauth-twitter'
gem 'omniauth-github'
gem 'omniauth-shibboleth'
gem 'omniauth-kerberos', group: :kerberos
gem 'omniauth-gitlab'
gem 'omniauth-bitbucket'
gem 'omniauth-google-oauth2', '~> 0.2.5'
gem 'omniauth-twitter', '~> 1.0.1'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-shibboleth', '~> 1.1.1'
gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-saml', '~> 1.4.0'
gem 'doorkeeper', '2.1.3'
gem 'doorkeeper', '~> 2.1.3'
gem "rack-oauth2", "~> 1.0.5"
# Two-factor authentication
gem 'devise-two-factor'
gem 'rqrcode-rails3'
gem 'attr_encrypted', '1.3.4'
gem 'devise-two-factor', '~> 1.0.1'
gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 1.3.4'
# Browser detection
gem "browser", '~> 0.8.0'
@@ -48,7 +56,7 @@ gem 'gitlab-grack', '~> 2.0.2', require: 'grack'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap"
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki
gem 'gollum-lib', '~> 4.0.2'
@@ -63,47 +71,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
# API
gem "grape", "~> 0.6.1"
gem "grape-entity", "~> 0.4.2"
gem 'rack-cors', require: 'rack/cors'
gem 'rack-cors', '~> 0.2.9', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
gem "stamp"
gem "stamp", '~> 0.5.0'
# Enumeration fields
gem 'enumerize'
gem 'enumerize', '~> 0.7.0'
# Pagination
gem "kaminari", "~> 0.15.1"
# HAML
gem "haml-rails"
gem "haml-rails", '~> 0.5.3'
# Files attachments
gem "carrierwave"
gem "carrierwave", '~> 0.9.0'
# Drag and Drop UI
gem 'dropzonejs-rails'
gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage
gem "fog", "~> 1.25.0"
gem "unf"
gem "unf", '~> 0.1.4'
# Authorization
gem "six"
gem "six", '~> 0.2.0'
# Seed data
gem "seed-fu"
gem "seed-fu", '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '1.0.2', require: 'task_list/railtie'
gem 'github-markup'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'github-markup', '~> 1.3.1'
gem 'redcarpet', '~> 3.3.2'
gem 'RedCloth'
gem 'RedCloth', '~> 4.2.9'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1'
gem 'wikicloth', '~> 0.8.1'
gem 'asciidoctor', '~> 1.5.2'
# Diffs
@@ -111,37 +119,37 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
gem "unicorn", '~> 4.6.3'
gem 'unicorn-worker-killer'
gem "unicorn", '~> 4.8.2'
gem 'unicorn-worker-killer', '~> 0.4.2'
end
# State machine
gem "state_machine"
gem "state_machine", '~> 1.2.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
gem 'slim'
gem 'sinatra', require: nil
gem 'slim', '~> 2.0.2'
gem 'sinatra', '~> 1.4.4', require: nil
gem 'sidekiq', '~> 3.3'
gem 'sidetiq', '0.6.3'
gem 'sidetiq', '~> 0.6.3'
# HTTP requests
gem "httparty"
gem "httparty", '~> 0.13.3'
# Colored output to console
gem "colored"
gem "colored", '~> 1.2'
# GitLab settings
gem 'settingslogic'
gem 'settingslogic', '~> 2.0.9'
# Misc
gem "foreman"
gem 'version_sorter'
gem 'version_sorter', '~> 2.0.0'
# Cache
gem "redis-rails"
gem "redis-rails", '~> 4.0.0'
# Campfire integration
gem 'tinder', '~> 1.9.2'
@@ -177,69 +185,70 @@ gem "sanitize", '~> 2.0'
gem "rack-attack", '~> 4.3.0'
# Ace editor
gem 'ace-rails-ap'
gem 'ace-rails-ap', '~> 2.0.1'
# Keyboard shortcuts
gem 'mousetrap-rails'
gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes'
gem 'charlock_holmes', '~> 0.6.9.4'
gem "sass-rails", '~> 4.0.5'
gem "coffee-rails"
gem "uglifier"
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.3.2'
gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks'
gem 'jquery-turbolinks', '~> 2.0.1'
gem 'addressable'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.1'
gem 'gon', '~> 5.0.0'
gem 'jquery-atwho-rails', '~> 1.0.0'
gem 'jquery-rails', '3.1.3'
gem 'jquery-scrollto-rails'
gem 'jquery-ui-rails'
gem 'nprogress-rails'
gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 4.2.1'
gem 'nprogress-rails', '~> 0.1.2.3'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store'
gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus'
gem 'virtus', '~> 1.0.1'
group :development do
gem "foreman"
gem 'brakeman', require: false
gem "annotate", "~> 2.6.0.beta2"
gem "letter_opener"
gem 'quiet_assets', '~> 1.0.1'
gem 'rack-mini-profiler', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
gem 'quiet_assets', '~> 1.0.2'
gem 'rack-mini-profiler', '~> 0.9.0', require: false
gem 'rerun', '~> 0.10.0'
# Better errors handler
gem 'better_errors'
gem 'binding_of_caller'
gem 'better_errors', '~> 1.0.1'
gem 'binding_of_caller', '~> 0.7.2'
# Docs generator
gem "sdoc"
gem "sdoc", '~> 0.3.20'
# thin instead webrick
gem 'thin'
gem 'thin', '~> 1.6.1'
end
group :development, :test do
gem 'awesome_print'
gem 'byebug', platform: :mri
gem 'fuubar', '~> 2.0.0'
gem 'pry-rails'
gem 'coveralls', '~> 0.8.2', require: false
gem 'awesome_print', '~> 1.2.0'
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails'
gem 'factory_girl_rails', '~> 4.3.0'
gem 'rspec-rails', '~> 3.3.0'
gem 'rubocop', '0.28.0', require: false
gem 'spinach-rails'
gem 'spinach-rails', '~> 0.2.1'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.3.0'
gem 'minitest', '~> 5.7.0'
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
@@ -249,30 +258,57 @@ group :development, :test do
gem 'poltergeist', '~> 1.6.0'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine'
gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.3.1'
gem 'spring-commands-rspec', '~> 1.0.0'
gem 'spring', '~> 1.3.6'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.28.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
end
group :test do
gem 'simplecov', require: false
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit'
gem 'test_after_commit', '~> 0.2.2'
end
group :production do
gem "gitlab_meta", '7.0'
end
gem "newrelic_rpm"
gem "newrelic_rpm", '~> 3.9.4.245'
gem 'octokit', '3.7.0'
gem 'octokit', '~> 3.7.0'
gem "mail_room", "~> 0.4.0"
gem 'email_reply_parser'
gem 'email_reply_parser', '~> 0.5.8'
## CI
gem 'activerecord-deprecated_finders', '~> 1.0.3'
gem 'activerecord-session_store', '~> 0.1.0'
gem "nested_form", '~> 0.3.2'
# Scheduled
gem 'whenever', '~> 0.8.4', require: false
# OAuth
gem 'oauth2', '~> 1.0.0'
gem 'gitlab_ci_meta', '~> 4.0'
# Soft deletion
gem "paranoia", "~> 2.0"
group :development, :test do
gem 'guard-rspec', '~> 4.2.0'
gem 'rb-fsevent', require: darwin_only('rb-fsevent')
gem 'growl', require: darwin_only('growl')
gem 'rb-inotify', require: linux_only('rb-inotify')
end
+294 -250
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,3 +1,3 @@
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default
worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+39
View File
@@ -0,0 +1,39 @@
var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}};
@@ -0,0 +1,50 @@
# This is a manifest file that'll be compiled into application.js, which will include all the files
# listed below.
#
# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
#
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
# GO AFTER THE REQUIRES BELOW.
#
#= require jquery
#= require bootstrap
#= require jquery_ujs
#= require turbolinks
#= require jquery.turbolinks
#= require jquery.endless-scroll
#= require pager
#= require nprogress
#= require nprogress-turbolinks
#= require jquery_nested_form
#= require_tree .
#
#
$(document).on 'click', '.edit-runner-link', (event) ->
event.preventDefault()
descr = $(this).closest('.runner-description').first()
descr.addClass('hide')
form = descr.next('.runner-description-form')
descrInput = form.find('input.description')
originalValue = descrInput.val()
form.removeClass('hide')
form.find('.cancel').on 'click', (event) ->
event.preventDefault()
form.addClass('hide')
descrInput.val(originalValue)
descr.removeClass('hide')
$(document).on 'click', '.assign-all-runner', ->
$(this).replaceWith('<i class="fa-refresh fa-spin"></i> Assign in progress..')
window.unbindEvents = ->
$(document).unbind('scroll')
$(document).off('scroll')
document.addEventListener("page:fetch", unbindEvents)
+41
View File
@@ -0,0 +1,41 @@
class CiBuild
@interval: null
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
#
$("#autoscroll-button").bind "click", ->
state = $(this).data("state")
if "enabled" is state
$(this).data "state", "disabled"
$(this).text "enable autoscroll"
else
$(this).data "state", "enabled"
$(this).text "disable autoscroll"
#
# Check for new build output if user still watching build page
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
if window.location.href is build_url
$.ajax
url: build_url
dataType: "json"
success: (build) =>
if build.status == "running"
$('#build-trace code').html build.trace_html
$('#build-trace code').append '<i class="fa-refresh fa-spin"/>'
@checkAutoscroll()
else
Turbolinks.visit build_url
, 4000
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
@CiBuild = CiBuild
+42
View File
@@ -0,0 +1,42 @@
@CiPager =
init: (@url, @limit = 0, preload, @disable = false) ->
if preload
@offset = 0
@getItems()
else
@offset = @limit
@initLoadMore()
getItems: ->
$(".loading").show()
$.ajax
type: "GET"
url: @url
data: "limit=" + @limit + "&offset=" + @offset
complete: =>
$(".loading").hide()
success: (data) =>
Pager.append(data.count, data.html)
dataType: "json"
append: (count, html) ->
if count > 1
$(".content-list").append html
if count == @limit
@offset += count
else
@disable = true
initLoadMore: ->
$(document).unbind('scroll')
$(document).endlessScroll
bottomPixels: 400
fireDelay: 1000
fireOnce: true
ceaseFire: ->
Pager.disable
callback: (i) =>
unless $(".loading").is(':visible')
$(".loading").show()
Pager.getItems()
@@ -0,0 +1,6 @@
$(document).on 'click', '.badge-codes-toggle', ->
$('.badge-codes-block').toggleClass("hide")
return false
$(document).on 'click', '.sync-now', ->
$(this).find('i').addClass('fa-spin')
@@ -0,0 +1,46 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the top of the
* compiled file, but it's generally better to create a new file per style scope.
*
*= require_self
*/
@import "main/variables.scss";
@import "main/mixins.scss";
@import "main/fonts.scss";
@import "main/layout.scss";
/**
* Twitter bootstrap
*/
@import 'bootstrap';
/**
* Font icons
*
*/
@import "font-awesome";
/**
* Generic css (forms, nav etc):
*/
@import "generic/*";
/**
* Page specific styles (issues, projects etc):
*/
@import "sections/*";
/*
* NProgress
*/
$nprogress-color: #9BC;
@import 'nprogress';
@import 'nprogress-bootstrap';
@@ -0,0 +1,29 @@
.avatar {
float: left;
margin-right: 12px;
width: 40px;
height: 40px;
padding: 0;
@include border-radius($avatar_radius);
&.avatar-inline {
float: none;
margin-left: 4px;
margin-bottom: 2px;
&.s16 { margin-right: 4px; }
&.s24 { margin-right: 4px; }
}
&.avatar-tile {
@include border-radius(0px);
}
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
&.s24 { width: 24px; height: 24px; margin-right: 8px; }
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
&.s90 { width: 90px; height: 90px; margin-right: 15px; }
&.s160 { width: 160px; height: 160px; margin-right: 20px; }
}
@@ -0,0 +1,7 @@
.btn {
@extend .btn-default;
&.btn-save {
@extend .btn-primary;
}
}
@@ -0,0 +1,45 @@
/*
* Callouts from Bootstrap3 docs
*
* Not quite alerts, but custom and helpful notes for folks reading the docs.
* Requires a base and modifier class.
*/
/* Common styles for all types */
.bs-callout {
margin: 20px 0;
padding: 20px;
border-left: 3px solid #eee;
color: #666;
background: #f9f9f9;
}
.bs-callout h4 {
margin-top: 0;
margin-bottom: 5px;
}
.bs-callout p:last-child {
margin-bottom: 0;
}
/* Variations */
.bs-callout-danger {
background-color: #fdf7f7;
border-color: #eed3d7;
color: #b94a48;
}
.bs-callout-warning {
background-color: #faf8f0;
border-color: #faebcc;
color: #8a6d3b;
}
.bs-callout-info {
background-color: #f4f8fa;
border-color: #bce8f1;
color: #34789a;
}
.bs-callout-success {
background-color: #dff0d8;
border-color: #5cA64d;
color: #3c763d;
}
@@ -0,0 +1,189 @@
/** COLORS **/
.cgray { color: gray }
.clgray { color: #BBB }
.cred { color: #D12F19 }
.cgreen { color: #4a2 }
.cblue { color: #29A }
.cblack { color: #111 }
.cdark { color: #444 }
.camber { color: #ffc000 }
.cwhite { color: #fff!important }
.bgred { background: #F2DEDE!important }
/** COMMON CLASSES **/
.prepend-top-10 { margin-top:10px }
.prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px }
.prepend-left-20 { margin-left:20px }
.append-right-10 { margin-right:10px }
.append-right-20 { margin-right:20px }
.append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px }
.inline { display: inline-block }
.padded { padding:20px }
.ipadded { padding:20px!important }
.lborder { border-left:1px solid #eee }
.underlined_link { text-decoration: underline; }
.hint { font-style: italic; color: #999; }
.light { color: #888 }
.tiny { font-weight: normal }
.vtop { vertical-align: top !important; }
.dropdown-menu > li > a {
text-shadow: none;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background: #29b;
}
.breadcrumb > li + li:before {
content: "/";
padding: 0;
color: #666;
}
.str-truncated {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
max-width: 82%;
}
.page-title {
color: #444;
line-height: 1.5;
margin-top: 0px;
margin-bottom: 15px;
}
.slead {
margin-bottom: 18px;
font-size: 16px;
font-weight: normal;
line-height: 1.4;
}
.help-callout {
li {
font-size: 15px;
line-height: 1.6;
}
}
/** light list with border-bottom between li **/
ul.bordered-list {
margin: 5px 0px;
padding: 0px;
li {
padding: 5px 0;
border-bottom: 1px solid #EEE;
overflow: hidden;
display: block;
margin: 0px;
&:last-child { border:none }
&.active {
background: #f9f9f9;
a { font-weight: bold; }
}
}
&.top-list {
li:first-child {
padding-top: 0;
h4, h5 {
margin-top: 0;
}
}
}
}
.underlined-title {
border-bottom: 1px solid #ccc;
padding: 0 0 3px 3px;
}
// Nav tabs
.nav.nav-tabs {
li {
> a {
padding: 8px 20px;
margin-right: 7px;
line-height: 20px;
border-color: #EEE;
color: #888;
border-bottom: 1px solid #ddd;
.badge {
background-color: #eee;
color: #888;
text-shadow: 0 1px 1px #fff;
}
i[class^="fa-"] {
line-height: 14px;
}
}
&.active {
> a {
border-color: #CCC;
border-bottom: 1px solid #fff;
color: #333;
font-weight: bold;
}
}
}
&.nav-small-tabs > li > a {
padding: 6px 9px;
}
}
.nav-tabs > li > a,
.nav-pills > li > a {
color: #666;
}
.nav-small > li > a {
padding: 3px 5px;
font-size: 12px;
}
// Breadcrumb
ul.breadcrumb {
background: white;
border: none;
li {
display: inline;
text-shadow: 0 1px 0 white
}
a {
font-size: 16px;
}
}
/**
* fix to keep tooltips position in top navigation bar
*
*/
.navbar .nav > li {
position: relative;
white-space: nowrap;
}
// alerts
.alert-disabled {
background-color: #e6e6e6;
border-color: #ebccd1;
color: #b0b0b0;
}
.label {
margin-right: 5px;
font-weight: normal;
}
@@ -0,0 +1,28 @@
input[type='text'].danger {
background: #F2DEDE!important;
border-color: #D66;
text-shadow: 0 1px 1px #fff
}
fieldset {
margin-bottom: 25px;
}
.form-actions {
padding: 17px 20px 18px;
margin-top: 18px;
margin-bottom: 18px;
background-color: whitesmoke;
border-top: 1px solid #e5e5e5;
padding-left: 17%;
}
label {
&.control-label {
@extend .col-sm-2;
}
&.inline-label {
margin: 0;
}
}
@@ -0,0 +1,20 @@
table {
&.table {
tr {
td, th {
padding: 8px 10px;
line-height: 20px;
vertical-align: middle;
}
th {
font-weight: normal;
font-size: 15px;
border-bottom: 1px solid #CCC !important;
}
td {
border-color: #F1F1F1 !important;
border-bottom: 1px solid;
}
}
}
}
@@ -0,0 +1,63 @@
h6 {
color: #888;
text-transform: uppercase;
}
pre {
font-family: $monospace_font;
&.dark {
background: #333;
color: #f5f5f5;
}
}
/**
* Links
*
*/
a {
outline: none;
color: $link_color;
&:hover {
text-decoration: none;
color: $primary_color;
}
&:focus {
text-decoration: underline;
}
&.dark {
color: $style_color;
}
&.lined {
text-decoration: underline;
&:hover { text-decoration: underline; }
}
&.gray {
color: gray;
}
&.supp_diff_link {
text-align: center;
padding: 20px 0;
background: #f1f1f1;
width: 100%;
float: left;
}
&.neib {
margin-right: 15px;
}
}
a:focus {
outline: none;
}
.monospace {
font-family: $monospace_font;
}
@@ -0,0 +1,904 @@
// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
// see also: https://gist.github.com/jasonm23/2868981
$black: #000000;
$red: #cd0000;
$green: #00cd00;
$yellow: #cdcd00;
$blue: #0000ee; // according to wikipedia, this is the xterm standard
//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
$l-red: #ff0000;
$l-green: #00ff00;
$l-yellow: #ffff00;
$l-blue: #5c5cff;
$l-magenta: #ff00ff;
$l-cyan: #00ffff;
$l-white: #ffffff;
.term-bold {
font-weight: bold;
}
.term-italic {
font-style: italic;
}
.term-conceal {
visibility: hidden;
}
.term-underline {
text-decoration: underline;
}
.term-cross {
text-decoration: line-through;
}
.term-fg-black {
color: $black;
}
.term-fg-red {
color: $red;
}
.term-fg-green {
color: $green;
}
.term-fg-yellow {
color: $yellow;
}
.term-fg-blue {
color: $blue;
}
.term-fg-magenta {
color: $magenta;
}
.term-fg-cyan {
color: $cyan;
}
.term-fg-white {
color: $white;
}
.term-fg-l-black {
color: $l-black;
}
.term-fg-l-red {
color: $l-red;
}
.term-fg-l-green {
color: $l-green;
}
.term-fg-l-yellow {
color: $l-yellow;
}
.term-fg-l-blue {
color: $l-blue;
}
.term-fg-l-magenta {
color: $l-magenta;
}
.term-fg-l-cyan {
color: $l-cyan;
}
.term-fg-l-white {
color: $l-white;
}
.term-bg-black {
background-color: $black;
}
.term-bg-red {
background-color: $red;
}
.term-bg-green {
background-color: $green;
}
.term-bg-yellow {
background-color: $yellow;
}
.term-bg-blue {
background-color: $blue;
}
.term-bg-magenta {
background-color: $magenta;
}
.term-bg-cyan {
background-color: $cyan;
}
.term-bg-white {
background-color: $white;
}
.term-bg-l-black {
background-color: $l-black;
}
.term-bg-l-red {
background-color: $l-red;
}
.term-bg-l-green {
background-color: $l-green;
}
.term-bg-l-yellow {
background-color: $l-yellow;
}
.term-bg-l-blue {
background-color: $l-blue;
}
.term-bg-l-magenta {
background-color: $l-magenta;
}
.term-bg-l-cyan {
background-color: $l-cyan;
}
.term-bg-l-white {
background-color: $l-white;
}
.xterm-fg-0 {
color: #000000;
}
.xterm-fg-1 {
color: #800000;
}
.xterm-fg-2 {
color: #008000;
}
.xterm-fg-3 {
color: #808000;
}
.xterm-fg-4 {
color: #000080;
}
.xterm-fg-5 {
color: #800080;
}
.xterm-fg-6 {
color: #008080;
}
.xterm-fg-7 {
color: #c0c0c0;
}
.xterm-fg-8 {
color: #808080;
}
.xterm-fg-9 {
color: #ff0000;
}
.xterm-fg-10 {
color: #00ff00;
}
.xterm-fg-11 {
color: #ffff00;
}
.xterm-fg-12 {
color: #0000ff;
}
.xterm-fg-13 {
color: #ff00ff;
}
.xterm-fg-14 {
color: #00ffff;
}
.xterm-fg-15 {
color: #ffffff;
}
.xterm-fg-16 {
color: #000000;
}
.xterm-fg-17 {
color: #00005f;
}
.xterm-fg-18 {
color: #000087;
}
.xterm-fg-19 {
color: #0000af;
}
.xterm-fg-20 {
color: #0000d7;
}
.xterm-fg-21 {
color: #0000ff;
}
.xterm-fg-22 {
color: #005f00;
}
.xterm-fg-23 {
color: #005f5f;
}
.xterm-fg-24 {
color: #005f87;
}
.xterm-fg-25 {
color: #005faf;
}
.xterm-fg-26 {
color: #005fd7;
}
.xterm-fg-27 {
color: #005fff;
}
.xterm-fg-28 {
color: #008700;
}
.xterm-fg-29 {
color: #00875f;
}
.xterm-fg-30 {
color: #008787;
}
.xterm-fg-31 {
color: #0087af;
}
.xterm-fg-32 {
color: #0087d7;
}
.xterm-fg-33 {
color: #0087ff;
}
.xterm-fg-34 {
color: #00af00;
}
.xterm-fg-35 {
color: #00af5f;
}
.xterm-fg-36 {
color: #00af87;
}
.xterm-fg-37 {
color: #00afaf;
}
.xterm-fg-38 {
color: #00afd7;
}
.xterm-fg-39 {
color: #00afff;
}
.xterm-fg-40 {
color: #00d700;
}
.xterm-fg-41 {
color: #00d75f;
}
.xterm-fg-42 {
color: #00d787;
}
.xterm-fg-43 {
color: #00d7af;
}
.xterm-fg-44 {
color: #00d7d7;
}
.xterm-fg-45 {
color: #00d7ff;
}
.xterm-fg-46 {
color: #00ff00;
}
.xterm-fg-47 {
color: #00ff5f;
}
.xterm-fg-48 {
color: #00ff87;
}
.xterm-fg-49 {
color: #00ffaf;
}
.xterm-fg-50 {
color: #00ffd7;
}
.xterm-fg-51 {
color: #00ffff;
}
.xterm-fg-52 {
color: #5f0000;
}
.xterm-fg-53 {
color: #5f005f;
}
.xterm-fg-54 {
color: #5f0087;
}
.xterm-fg-55 {
color: #5f00af;
}
.xterm-fg-56 {
color: #5f00d7;
}
.xterm-fg-57 {
color: #5f00ff;
}
.xterm-fg-58 {
color: #5f5f00;
}
.xterm-fg-59 {
color: #5f5f5f;
}
.xterm-fg-60 {
color: #5f5f87;
}
.xterm-fg-61 {
color: #5f5faf;
}
.xterm-fg-62 {
color: #5f5fd7;
}
.xterm-fg-63 {
color: #5f5fff;
}
.xterm-fg-64 {
color: #5f8700;
}
.xterm-fg-65 {
color: #5f875f;
}
.xterm-fg-66 {
color: #5f8787;
}
.xterm-fg-67 {
color: #5f87af;
}
.xterm-fg-68 {
color: #5f87d7;
}
.xterm-fg-69 {
color: #5f87ff;
}
.xterm-fg-70 {
color: #5faf00;
}
.xterm-fg-71 {
color: #5faf5f;
}
.xterm-fg-72 {
color: #5faf87;
}
.xterm-fg-73 {
color: #5fafaf;
}
.xterm-fg-74 {
color: #5fafd7;
}
.xterm-fg-75 {
color: #5fafff;
}
.xterm-fg-76 {
color: #5fd700;
}
.xterm-fg-77 {
color: #5fd75f;
}
.xterm-fg-78 {
color: #5fd787;
}
.xterm-fg-79 {
color: #5fd7af;
}
.xterm-fg-80 {
color: #5fd7d7;
}
.xterm-fg-81 {
color: #5fd7ff;
}
.xterm-fg-82 {
color: #5fff00;
}
.xterm-fg-83 {
color: #5fff5f;
}
.xterm-fg-84 {
color: #5fff87;
}
.xterm-fg-85 {
color: #5fffaf;
}
.xterm-fg-86 {
color: #5fffd7;
}
.xterm-fg-87 {
color: #5fffff;
}
.xterm-fg-88 {
color: #870000;
}
.xterm-fg-89 {
color: #87005f;
}
.xterm-fg-90 {
color: #870087;
}
.xterm-fg-91 {
color: #8700af;
}
.xterm-fg-92 {
color: #8700d7;
}
.xterm-fg-93 {
color: #8700ff;
}
.xterm-fg-94 {
color: #875f00;
}
.xterm-fg-95 {
color: #875f5f;
}
.xterm-fg-96 {
color: #875f87;
}
.xterm-fg-97 {
color: #875faf;
}
.xterm-fg-98 {
color: #875fd7;
}
.xterm-fg-99 {
color: #875fff;
}
.xterm-fg-100 {
color: #878700;
}
.xterm-fg-101 {
color: #87875f;
}
.xterm-fg-102 {
color: #878787;
}
.xterm-fg-103 {
color: #8787af;
}
.xterm-fg-104 {
color: #8787d7;
}
.xterm-fg-105 {
color: #8787ff;
}
.xterm-fg-106 {
color: #87af00;
}
.xterm-fg-107 {
color: #87af5f;
}
.xterm-fg-108 {
color: #87af87;
}
.xterm-fg-109 {
color: #87afaf;
}
.xterm-fg-110 {
color: #87afd7;
}
.xterm-fg-111 {
color: #87afff;
}
.xterm-fg-112 {
color: #87d700;
}
.xterm-fg-113 {
color: #87d75f;
}
.xterm-fg-114 {
color: #87d787;
}
.xterm-fg-115 {
color: #87d7af;
}
.xterm-fg-116 {
color: #87d7d7;
}
.xterm-fg-117 {
color: #87d7ff;
}
.xterm-fg-118 {
color: #87ff00;
}
.xterm-fg-119 {
color: #87ff5f;
}
.xterm-fg-120 {
color: #87ff87;
}
.xterm-fg-121 {
color: #87ffaf;
}
.xterm-fg-122 {
color: #87ffd7;
}
.xterm-fg-123 {
color: #87ffff;
}
.xterm-fg-124 {
color: #af0000;
}
.xterm-fg-125 {
color: #af005f;
}
.xterm-fg-126 {
color: #af0087;
}
.xterm-fg-127 {
color: #af00af;
}
.xterm-fg-128 {
color: #af00d7;
}
.xterm-fg-129 {
color: #af00ff;
}
.xterm-fg-130 {
color: #af5f00;
}
.xterm-fg-131 {
color: #af5f5f;
}
.xterm-fg-132 {
color: #af5f87;
}
.xterm-fg-133 {
color: #af5faf;
}
.xterm-fg-134 {
color: #af5fd7;
}
.xterm-fg-135 {
color: #af5fff;
}
.xterm-fg-136 {
color: #af8700;
}
.xterm-fg-137 {
color: #af875f;
}
.xterm-fg-138 {
color: #af8787;
}
.xterm-fg-139 {
color: #af87af;
}
.xterm-fg-140 {
color: #af87d7;
}
.xterm-fg-141 {
color: #af87ff;
}
.xterm-fg-142 {
color: #afaf00;
}
.xterm-fg-143 {
color: #afaf5f;
}
.xterm-fg-144 {
color: #afaf87;
}
.xterm-fg-145 {
color: #afafaf;
}
.xterm-fg-146 {
color: #afafd7;
}
.xterm-fg-147 {
color: #afafff;
}
.xterm-fg-148 {
color: #afd700;
}
.xterm-fg-149 {
color: #afd75f;
}
.xterm-fg-150 {
color: #afd787;
}
.xterm-fg-151 {
color: #afd7af;
}
.xterm-fg-152 {
color: #afd7d7;
}
.xterm-fg-153 {
color: #afd7ff;
}
.xterm-fg-154 {
color: #afff00;
}
.xterm-fg-155 {
color: #afff5f;
}
.xterm-fg-156 {
color: #afff87;
}
.xterm-fg-157 {
color: #afffaf;
}
.xterm-fg-158 {
color: #afffd7;
}
.xterm-fg-159 {
color: #afffff;
}
.xterm-fg-160 {
color: #d70000;
}
.xterm-fg-161 {
color: #d7005f;
}
.xterm-fg-162 {
color: #d70087;
}
.xterm-fg-163 {
color: #d700af;
}
.xterm-fg-164 {
color: #d700d7;
}
.xterm-fg-165 {
color: #d700ff;
}
.xterm-fg-166 {
color: #d75f00;
}
.xterm-fg-167 {
color: #d75f5f;
}
.xterm-fg-168 {
color: #d75f87;
}
.xterm-fg-169 {
color: #d75faf;
}
.xterm-fg-170 {
color: #d75fd7;
}
.xterm-fg-171 {
color: #d75fff;
}
.xterm-fg-172 {
color: #d78700;
}
.xterm-fg-173 {
color: #d7875f;
}
.xterm-fg-174 {
color: #d78787;
}
.xterm-fg-175 {
color: #d787af;
}
.xterm-fg-176 {
color: #d787d7;
}
.xterm-fg-177 {
color: #d787ff;
}
.xterm-fg-178 {
color: #d7af00;
}
.xterm-fg-179 {
color: #d7af5f;
}
.xterm-fg-180 {
color: #d7af87;
}
.xterm-fg-181 {
color: #d7afaf;
}
.xterm-fg-182 {
color: #d7afd7;
}
.xterm-fg-183 {
color: #d7afff;
}
.xterm-fg-184 {
color: #d7d700;
}
.xterm-fg-185 {
color: #d7d75f;
}
.xterm-fg-186 {
color: #d7d787;
}
.xterm-fg-187 {
color: #d7d7af;
}
.xterm-fg-188 {
color: #d7d7d7;
}
.xterm-fg-189 {
color: #d7d7ff;
}
.xterm-fg-190 {
color: #d7ff00;
}
.xterm-fg-191 {
color: #d7ff5f;
}
.xterm-fg-192 {
color: #d7ff87;
}
.xterm-fg-193 {
color: #d7ffaf;
}
.xterm-fg-194 {
color: #d7ffd7;
}
.xterm-fg-195 {
color: #d7ffff;
}
.xterm-fg-196 {
color: #ff0000;
}
.xterm-fg-197 {
color: #ff005f;
}
.xterm-fg-198 {
color: #ff0087;
}
.xterm-fg-199 {
color: #ff00af;
}
.xterm-fg-200 {
color: #ff00d7;
}
.xterm-fg-201 {
color: #ff00ff;
}
.xterm-fg-202 {
color: #ff5f00;
}
.xterm-fg-203 {
color: #ff5f5f;
}
.xterm-fg-204 {
color: #ff5f87;
}
.xterm-fg-205 {
color: #ff5faf;
}
.xterm-fg-206 {
color: #ff5fd7;
}
.xterm-fg-207 {
color: #ff5fff;
}
.xterm-fg-208 {
color: #ff8700;
}
.xterm-fg-209 {
color: #ff875f;
}
.xterm-fg-210 {
color: #ff8787;
}
.xterm-fg-211 {
color: #ff87af;
}
.xterm-fg-212 {
color: #ff87d7;
}
.xterm-fg-213 {
color: #ff87ff;
}
.xterm-fg-214 {
color: #ffaf00;
}
.xterm-fg-215 {
color: #ffaf5f;
}
.xterm-fg-216 {
color: #ffaf87;
}
.xterm-fg-217 {
color: #ffafaf;
}
.xterm-fg-218 {
color: #ffafd7;
}
.xterm-fg-219 {
color: #ffafff;
}
.xterm-fg-220 {
color: #ffd700;
}
.xterm-fg-221 {
color: #ffd75f;
}
.xterm-fg-222 {
color: #ffd787;
}
.xterm-fg-223 {
color: #ffd7af;
}
.xterm-fg-224 {
color: #ffd7d7;
}
.xterm-fg-225 {
color: #ffd7ff;
}
.xterm-fg-226 {
color: #ffff00;
}
.xterm-fg-227 {
color: #ffff5f;
}
.xterm-fg-228 {
color: #ffff87;
}
.xterm-fg-229 {
color: #ffffaf;
}
.xterm-fg-230 {
color: #ffffd7;
}
.xterm-fg-231 {
color: #ffffff;
}
.xterm-fg-232 {
color: #080808;
}
.xterm-fg-233 {
color: #121212;
}
.xterm-fg-234 {
color: #1c1c1c;
}
.xterm-fg-235 {
color: #262626;
}
.xterm-fg-236 {
color: #303030;
}
.xterm-fg-237 {
color: #3a3a3a;
}
.xterm-fg-238 {
color: #444444;
}
.xterm-fg-239 {
color: #4e4e4e;
}
.xterm-fg-240 {
color: #585858;
}
.xterm-fg-241 {
color: #626262;
}
.xterm-fg-242 {
color: #6c6c6c;
}
.xterm-fg-243 {
color: #767676;
}
.xterm-fg-244 {
color: #808080;
}
.xterm-fg-245 {
color: #8a8a8a;
}
.xterm-fg-246 {
color: #949494;
}
.xterm-fg-247 {
color: #9e9e9e;
}
.xterm-fg-248 {
color: #a8a8a8;
}
.xterm-fg-249 {
color: #b2b2b2;
}
.xterm-fg-250 {
color: #bcbcbc;
}
.xterm-fg-251 {
color: #c6c6c6;
}
.xterm-fg-252 {
color: #d0d0d0;
}
.xterm-fg-253 {
color: #dadada;
}
.xterm-fg-254 {
color: #e4e4e4;
}
.xterm-fg-255 {
color: #eeeeee;
}
@@ -0,0 +1,2 @@
/** Typo **/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace;
@@ -0,0 +1,18 @@
html {
overflow-y: scroll;
&.touch .tooltip { display: none !important; }
}
body {
margin-bottom: 20px;
}
.container {
padding-top: 0;
z-index: 5;
}
.container .content {
margin: 0 0;
}
@@ -0,0 +1,31 @@
@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius;
}
@mixin linear-gradient($from, $to) {
background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
background-image: -webkit-linear-gradient($from, $to);
background-image: -moz-linear-gradient($from, $to);
background-image: -ms-linear-gradient($from, $to);
background-image: -o-linear-gradient($from, $to);
}
@mixin transition($transition) {
-webkit-transition: $transition;
-moz-transition: $transition;
-ms-transition: $transition;
-o-transition: $transition;
transition: $transition;
}
@@ -0,0 +1,44 @@
/**
* General Colors
*/
$primary_color: #2FA0BB;
$link_color: #3A89A3;
$style_color: #246;
$bg_style_color: #246;
$hover: #D9EDF7;
/*
* Success colors (green)
*/
$border_success: #019875;
$bg_success: #019875;
/*
* Danger colors (red)
*/
$border_danger: #d43f3a;
$bg_danger: #d9534f;
/*
* Primary colors (blue)
*/
$border_primary: #246;
$bg_primary: #246;
/*
* Warning colors (yellow)
*/
$bg_warning: #EB9532;
$border_warning: #EB9532;
/**
* Twitter bootstrap variables
*/
$font-size-base: 13px !default;
$nav-pills-active-link-hover-bg: $bg_style_color;
$pagination-active-bg: $bg_style_color;
/**
* Avatar variables
*/
$avatar_radius: 50%;
@@ -0,0 +1,54 @@
pre.trace {
background: #111111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
overflow: auto;
overflow-y: hidden;
font-size: 12px;
.fa-refresh {
font-size: 24px;
margin-left: 20px;
}
}
.autoscroll-container {
position: fixed;
bottom: 10px;
right: 20px;
z-index: 100;
}
.scroll-controls {
position: fixed;
bottom: 10px;
left: 20px;
z-index: 100;
a {
display: block;
margin-bottom: 5px;
}
}
.build-widget {
padding: 10px;
background: #f4f4f4;
margin-bottom: 20px;
border-radius: 4px;
.title {
margin-top: 0;
color: #666;
line-height: 1.5;
}
.attr-name {
color: #777;
}
}
@@ -0,0 +1,8 @@
.incorrect-syntax{
font-size: 19px;
color: red;
}
.correct-syntax{
font-size: 19px;
color: #47a447;
}
@@ -0,0 +1,13 @@
.login-block {
padding: 15px;
margin: 0 auto;
text-align: center;
p {
font-size: 15px;
}
.btn-login {
padding: 18px 32px;
}
}
@@ -0,0 +1,54 @@
.navbar-static-top {
margin-bottom: 20px;
}
.navbar-ci {
background: $style_color;
.navbar-brand {
color: #fff;
&:hover {
color: #fff;
}
}
.brand,
.nav > li > a {
color: #fff;
&:hover, &:focus, &:active {
background: none;
}
}
.profile-holder {
position: relative;
img {
position: absolute;
top: -8px;
width: 32px;
@include border-radius(32px);
}
span {
margin-left: 42px;
}
}
.btn-login {
padding: 7px 22px;
margin-top: 7px;
&:hover, &:active, &:focus {
background: #018865 !important;
}
}
}
.turbolink-spinner {
position: absolute;
top: 11px;
left: 50%;
color: #FFF;
font-size: 20px;
}
@@ -0,0 +1,61 @@
.project-title {
margin: 0;
color: #444;
font-size: 20px;
line-height: 1.5;
}
.builds {
@extend .table;
.build {
&.alert{
margin-bottom: 6px;
}
}
}
.projects-table {
td {
vertical-align: middle !important;
}
}
.commit-info {
font-size: 14px;
.attr-name {
font-weight: 300;
color: #666;
margin-right: 5px;
}
pre.commit-message {
font-size: 14px;
background: none;
padding: 0;
margin: 0;
border: none;
margin: 20px 0;
border-bottom: 1px solid #EEE;
padding-bottom: 20px;
border-radius: 0;
}
}
.search{
width: 300px;
.search-input{
height: 35px;
}
form{
margin-top: 0;
margin-bottom: 0;
}
}
.loading{
font-size: 20px;
}
@@ -0,0 +1,34 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
color: #FFF;
&.runner-state-shared {
background: #32b186;
}
&.runner-state-specific {
background: #3498db;
}
}
.runner-status-online {
color: green;
}
.runner-status-offline {
color: gray;
}
.runner-status-paused {
color: red;
}
.runner {
.btn {
padding: 1px 6px;
}
h4 {
font-weight: normal;
}
}
@@ -0,0 +1,11 @@
.welcome-block {
margin-top: 50px;
color: #555;
font-size: 16px;
line-height: 1.5;
h1, h2, h3 {
font-weight: bold;
margin-bottom: 20px;
}
}
+6 -5
View File
@@ -1,9 +1,13 @@
require 'gon'
class ApplicationController < ActionController::Base
def self.railtie_helpers_paths
"app/helpers/gitlab"
end
include Gitlab::CurrentSettings
include GitlabRoutingHelper
include PageLayoutHelper
include Gitlab::GitlabRoutingHelper
include Gitlab::PageLayoutHelper
PER_PAGE = 20
@@ -131,9 +135,6 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
rescue Grit::NoSuchPathError => e
log_exception(e)
nil
end
def authorize_project!(action)
@@ -0,0 +1,10 @@
module Ci
module Admin
class ApplicationController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :authenticate_admin!
layout "ci/admin"
end
end
end
@@ -0,0 +1,31 @@
module Ci
class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController
before_action :set_application_setting
def show
end
def update
if @application_setting.update_attributes(application_setting_params)
redirect_to ci_admin_application_settings_path,
notice: 'Application settings saved successfully'
else
render :show
end
end
private
def set_application_setting
@application_setting = Ci::ApplicationSetting.current
@application_setting ||= Ci::ApplicationSetting.create_from_defaults
end
def application_setting_params
params.require(:application_setting).permit(
:all_broken_builds,
:add_pusher,
)
end
end
end
@@ -0,0 +1,12 @@
module Ci
class Admin::BuildsController < Ci::Admin::ApplicationController
def index
@scope = params[:scope]
@builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30)
if ["pending", "running"].include? @scope
@builds = @builds.send(@scope)
end
end
end
end
@@ -0,0 +1,9 @@
module Ci
class Admin::EventsController < Ci::Admin::ApplicationController
EVENTS_PER_PAGE = 50
def index
@events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE)
end
end
end
@@ -0,0 +1,19 @@
module Ci
class Admin::ProjectsController < Ci::Admin::ApplicationController
def index
@projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30)
end
def destroy
project.destroy
redirect_to ci_projects_url
end
protected
def project
@project ||= Ci::Project.find(params[:id])
end
end
end
@@ -0,0 +1,34 @@
module Ci
class Admin::RunnerProjectsController < Ci::Admin::ApplicationController
layout 'ci/project'
def index
@runner_projects = project.runner_projects.all
@runner_project = project.runner_projects.new
end
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
if @runner.assign_to(project, current_user)
redirect_to ci_admin_runner_path(@runner)
else
redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project'
end
end
def destroy
rp = Ci::RunnerProject.find(params[:id])
runner = rp.runner
rp.destroy
redirect_to ci_admin_runner_path(runner)
end
private
def project
@project ||= Ci::Project.find(params[:project_id])
end
end
end
@@ -0,0 +1,69 @@
module Ci
class Admin::RunnersController < Ci::Admin::ApplicationController
before_filter :runner, except: :index
def index
@runners = Ci::Runner.order('id DESC')
@runners = @runners.search(params[:search]) if params[:search].present?
@runners = @runners.page(params[:page]).per(30)
@active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count
end
def show
@builds = @runner.builds.order('id DESC').first(30)
@projects = Ci::Project.all
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.where("projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end
def update
@runner.update_attributes(runner_params)
respond_to do |format|
format.js
format.html { redirect_to ci_admin_runner_path(@runner) }
end
end
def destroy
@runner.destroy
redirect_to ci_admin_runners_path
end
def resume
if @runner.update_attributes(active: true)
redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
end
end
def pause
if @runner.update_attributes(active: false)
redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
end
end
def assign_all
Ci::Project.unassigned(@runner).all.each do |project|
@runner.assign_to(project, current_user)
end
redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects"
end
private
def runner
@runner ||= Ci::Runner.find(params[:id])
end
def runner_params
params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active)
end
end
end
@@ -0,0 +1,133 @@
module Ci
class ApplicationController < ActionController::Base
def self.railtie_helpers_paths
"app/helpers/ci"
end
include Ci::UserSessionsHelper
rescue_from Ci::Network::UnauthorizedError, with: :invalid_token
before_filter :default_headers
before_filter :check_config
protect_from_forgery
helper_method :current_user
before_filter :reset_cache
private
def current_user
@current_user ||= session[:ci_current_user]
end
def sign_in(user)
session[:ci_current_user] = user
end
def sign_out
reset_session
end
def authenticate_user!
unless current_user
redirect_to new_ci_user_sessions_path
return
end
end
def authenticate_admin!
unless current_user && current_user.is_admin
redirect_to new_ci_user_sessions_path
return
end
end
def authenticate_public_page!
unless project.public
unless current_user
redirect_to(new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath))) and return
end
unless current_user.can_access_project?(project.gitlab_id)
page_404 and return
end
end
end
def authenticate_token!
unless project.valid_token?(params[:token])
return head(403)
end
end
def authorize_access_project!
unless current_user.can_access_project?(@project.gitlab_id)
return page_404
end
end
def authorize_project_developer!
unless current_user.has_developer_access?(@project.gitlab_id)
return page_404
end
end
def authorize_manage_project!
unless current_user.can_manage_project?(@project.gitlab_id)
return page_404
end
end
def page_404
render file: "#{Rails.root}/public/404.html", status: 404, layout: false
end
# Reset user cache every day for security purposes
def reset_cache
if current_user && current_user.sync_at < (Time.zone.now - 24.hours)
current_user.reset_cache
end
end
def default_headers
headers['X-Frame-Options'] = 'DENY'
headers['X-XSS-Protection'] = '1; mode=block'
end
# JSON for infinite scroll via Pager object
def pager_json(partial, count)
html = render_to_string(
partial,
layout: false,
formats: [:html]
)
render json: {
html: html,
count: count
}
end
def check_config
redirect_to oauth2_ci_help_path unless valid_config?
end
def valid_config?
server = GitlabCi.config.gitlab_server
if server.blank? || server.url.blank? || server.app_id.blank? || server.app_secret.blank?
false
else
true
end
rescue Settingslogic::MissingSetting, NoMethodError
false
end
def invalid_token
reset_session
redirect_to ci_root_path
end
end
end
+77
View File
@@ -0,0 +1,77 @@
module Ci
class BuildsController < Ci::ApplicationController
before_filter :authenticate_user!, except: [:status, :show]
before_filter :authenticate_public_page!, only: :show
before_filter :project
before_filter :authorize_access_project!, except: [:status, :show]
before_filter :authorize_manage_project!, except: [:status, :show, :retry, :cancel]
before_filter :authorize_project_developer!, only: [:retry, :cancel]
before_filter :build, except: [:show]
def show
if params[:id] =~ /\A\d+\Z/
@build = build
else
# try to find commit by sha
commit = commit_by_sha
if commit
# Redirect to commit page
redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha)
return
end
end
raise ActiveRecord::RecordNotFound unless @build
@builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
@commit = @build.commit
respond_to do |format|
format.html
format.json do
render json: @build.to_json(methods: :trace_html)
end
end
end
def retry
if @build.commands.blank?
return page_404
end
build = Ci::Build.retry(@build)
if params[:return_to]
redirect_to URI.parse(params[:return_to]).path
else
redirect_to ci_project_build_path(project, build)
end
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
def cancel
@build.cancel
redirect_to ci_project_build_path(@project, @build)
end
protected
def project
@project = Ci::Project.find(params[:project_id])
end
def build
@build ||= project.builds.unscoped.find_by(id: params[:id])
end
def commit_by_sha
@project.commits.find_by(sha: params[:id])
end
end
end
+24
View File
@@ -0,0 +1,24 @@
module Ci
class ChartsController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
layout 'ci/project'
def show
@charts = {}
@charts[:week] = Ci::Charts::WeekChart.new(@project)
@charts[:month] = Ci::Charts::MonthChart.new(@project)
@charts[:year] = Ci::Charts::YearChart.new(@project)
@charts[:build_times] = Ci::Charts::BuildTime.new(@project)
end
protected
def project
@project = Ci::Project.find(params[:project_id])
end
end
end
+37
View File
@@ -0,0 +1,37 @@
module Ci
class CommitsController < Ci::ApplicationController
before_filter :authenticate_user!, except: [:status, :show]
before_filter :authenticate_public_page!, only: :show
before_filter :project
before_filter :authorize_access_project!, except: [:status, :show, :cancel]
before_filter :authorize_project_developer!, only: [:cancel]
before_filter :commit, only: :show
def show
@builds = @commit.builds
end
def status
commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage])
rescue ActiveRecord::RecordNotFound
render json: { status: "not_found" }
end
def cancel
commit.builds.running_or_pending.each(&:cancel)
redirect_to ci_project_ref_commit_path(project, commit.ref, commit.sha)
end
private
def project
@project ||= Ci::Project.find(params[:project_id])
end
def commit
@commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
end
end
end
+21
View File
@@ -0,0 +1,21 @@
module Ci
class EventsController < Ci::ApplicationController
EVENTS_PER_PAGE = 50
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_manage_project!
layout 'ci/project'
def index
@events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE)
end
private
def project
@project ||= Ci::Project.find(params[:project_id])
end
end
end
+16
View File
@@ -0,0 +1,16 @@
module Ci
class HelpsController < Ci::ApplicationController
skip_filter :check_config
def show
end
def oauth2
if valid_config?
redirect_to ci_root_path
else
render layout: 'ci/empty'
end
end
end
end
+26
View File
@@ -0,0 +1,26 @@
module Ci
class LintsController < Ci::ApplicationController
before_filter :authenticate_user!
def show
end
def create
if params[:content].blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
@config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
@stages = @config_processor.stages
@builds = @config_processor.builds
@status = true
end
rescue Ci::GitlabCiYamlProcessor::ValidationError => e
@error = e.message
@status = false
rescue Exception => e
@error = "Undefined error"
@status = false
end
end
end
+136
View File
@@ -0,0 +1,136 @@
module Ci
class ProjectsController < Ci::ApplicationController
PROJECTS_BATCH = 100
before_filter :authenticate_user!, except: [:build, :badge, :index, :show]
before_filter :authenticate_public_page!, only: :show
before_filter :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
before_filter :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create]
before_filter :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
before_filter :authenticate_token!, only: [:build]
before_filter :no_cache, only: [:badge]
protect_from_forgery except: :build
layout 'ci/project', except: [:index, :gitlab]
def index
@projects = Ci::Project.ordered_by_last_commit_date.public_only.page(params[:page]) unless current_user
end
def gitlab
@limit, @offset = (params[:limit] || PROJECTS_BATCH).to_i, (params[:offset] || 0).to_i
@page = @offset == 0 ? 1 : (@offset / @limit + 1)
current_user.reset_cache if params[:reset_cache]
@gl_projects = current_user.gitlab_projects(params[:search], @page, @limit)
@projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date
@total_count = @gl_projects.size
@gl_projects.reject! { |gl_project| @projects.map(&:gitlab_id).include?(gl_project.id) }
respond_to do |format|
format.json do
pager_json("ci/projects/gitlab", @total_count)
end
end
rescue Ci::Network::UnauthorizedError
raise
rescue
@error = 'Failed to fetch GitLab projects'
end
def show
@ref = params[:ref]
@commits = @project.commits.reverse_order
@commits = @commits.where(ref: @ref) if @ref
@commits = @commits.page(params[:page]).per(20)
end
def integration
end
def create
project_data = OpenStruct.new(JSON.parse(params["project"]))
unless current_user.can_manage_project?(project_data.id)
return redirect_to ci_root_path, alert: 'You have to have at least master role to enable CI for this project'
end
@project = Ci::CreateProjectService.new.execute(current_user, project_data, ci_project_url(":project_id"))
if @project.persisted?
redirect_to ci_project_path(@project, show_guide: true), notice: 'Project was successfully created.'
else
redirect_to :back, alert: 'Cannot save project'
end
end
def edit
end
def update
if project.update_attributes(project_params)
Ci::EventService.new.change_project_settings(current_user, project)
redirect_to :back, notice: 'Project was successfully updated.'
else
render action: "edit"
end
end
def destroy
project.destroy
Ci::Network.new.disable_ci(project.gitlab_id, current_user.authenticate_options)
Ci::EventService.new.remove_project(current_user, project)
redirect_to ci_projects_url
end
def build
@commit = Ci::CreateCommitService.new.execute(@project, params.dup)
if @commit && @commit.valid?
head 201
else
head 400
end
end
# Project status badge
# Image with build status for sha or ref
def badge
image = Ci::ImageForBuildService.new.execute(@project, params)
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
redirect_to :back
end
def dumped_yaml
send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml'
end
protected
def project
@project ||= Ci::Project.find(params[:id])
end
def no_cache
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
def project_params
params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
:polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
:email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
{ variables_attributes: [:id, :key, :value, :_destroy] })
end
end
end
@@ -0,0 +1,34 @@
module Ci
class RunnerProjectsController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_manage_project!
layout 'ci/project'
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) unless current_user.authorized_runners.include?(@runner)
if @runner.assign_to(project, current_user)
redirect_to ci_project_runners_path(project)
else
redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project'
end
end
def destroy
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
redirect_to ci_project_runners_path(project)
end
private
def project
@project ||= Ci::Project.find(params[:project_id])
end
end
end
+71
View File
@@ -0,0 +1,71 @@
module Ci
class RunnersController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
layout 'ci/project'
def index
@runners = @project.runners.order('id DESC')
@specific_runners = current_user.authorized_runners.
where.not(id: @runners).order('runners.id DESC').page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
def edit
end
def update
if @runner.update_attributes(runner_params)
redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
else
redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.'
end
end
def destroy
if @runner.only_for?(@project)
@runner.destroy
end
redirect_to ci_project_runners_path(@project)
end
def resume
if @runner.update_attributes(active: true)
redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
else
redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
end
end
def pause
if @runner.update_attributes(active: false)
redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
else
redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
end
end
def show
end
protected
def project
@project = Ci::Project.find(params[:project_id])
end
def set_runner
@runner ||= @project.runners.find(params[:id])
end
def runner_params
params.require(:runner).permit(:description, :tag_list, :contacted_at, :active)
end
end
end
+59
View File
@@ -0,0 +1,59 @@
module Ci
class ServicesController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
before_filter :service, only: [:edit, :update, :test]
respond_to :html
layout 'ci/project'
def index
@project.build_missing_services
@services = @project.services.reload
end
def edit
end
def update
if @service.update_attributes(service_params)
redirect_to edit_ci_project_service_path(@project, @service.to_param), notice: 'Service was successfully updated.'
else
render 'edit'
end
end
def test
last_build = @project.builds.last
if @service.execute(last_build)
message = { notice: 'We successfully tested the service' }
else
message = { alert: 'We tried to test the service but error occurred' }
end
redirect_to :back, message
end
private
def project
@project = Ci::Project.find(params[:project_id])
end
def service
@service ||= @project.services.find { |service| service.to_param == params[:id] }
end
def service_params
params.require(:service).permit(
:type, :active, :webhook, :notify_only_broken_builds,
:email_recipients, :email_only_broken_builds, :email_add_pusher,
:hipchat_token, :hipchat_room, :hipchat_server
)
end
end
end
+43
View File
@@ -0,0 +1,43 @@
module Ci
class TriggersController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
layout 'ci/project'
def index
@triggers = @project.triggers
@trigger = Ci::Trigger.new
end
def create
@trigger = @project.triggers.new
@trigger.save
if @trigger.valid?
redirect_to ci_project_triggers_path(@project)
else
@triggers = @project.triggers.select(&:persisted?)
render :index
end
end
def destroy
trigger.destroy
redirect_to ci_project_triggers_path(@project)
end
private
def trigger
@trigger ||= @project.triggers.find(params[:id])
end
def project
@project = Ci::Project.find(params[:project_id])
end
end
end
@@ -0,0 +1,65 @@
module Ci
class UserSessionsController < Ci::ApplicationController
before_filter :authenticate_user!, except: [:new, :callback, :auth]
def show
@user = current_user
end
def new
end
def auth
unless is_oauth_state_valid?(params[:state])
redirect_to new_ci_user_sessions_path
return
end
redirect_to client.auth_code.authorize_url({
redirect_uri: callback_ci_user_sessions_url,
state: params[:state]
})
end
def callback
unless is_oauth_state_valid?(params[:state])
redirect_to new_ci_user_sessions_path
return
end
token = client.auth_code.get_token(params[:code], redirect_uri: callback_ci_user_sessions_url).token
@user_session = Ci::UserSession.new
user = @user_session.authenticate(access_token: token)
if user && sign_in(user)
return_to = get_ouath_state_return_to(params[:state])
redirect_to(return_to || ci_root_path)
else
@error = 'Invalid credentials'
render :new
end
end
def destroy
sign_out
redirect_to new_ci_user_sessions_path
end
protected
def client
@client ||= ::OAuth2::Client.new(
GitlabCi.config.gitlab_server.app_id,
GitlabCi.config.gitlab_server.app_secret,
{
site: GitlabCi.config.gitlab_server.url,
authorize_url: 'oauth/authorize',
token_url: 'oauth/token'
}
)
end
end
end
@@ -0,0 +1,33 @@
module Ci
class VariablesController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
layout 'ci/project'
def show
end
def update
if project.update_attributes(project_params)
Ci::EventService.new.change_project_settings(current_user, project)
redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.'
else
render action: 'show'
end
end
private
def project
@project ||= Ci::Project.find(params[:project_id])
end
def project_params
params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
end
end
end
@@ -0,0 +1,53 @@
module Ci
class WebHooksController < Ci::ApplicationController
before_filter :authenticate_user!
before_filter :project
before_filter :authorize_access_project!
before_filter :authorize_manage_project!
layout 'ci/project'
def index
@web_hooks = @project.web_hooks
@web_hook = Ci::WebHook.new
end
def create
@web_hook = @project.web_hooks.new(web_hook_params)
@web_hook.save
if @web_hook.valid?
redirect_to ci_project_web_hooks_path(@project)
else
@web_hooks = @project.web_hooks.select(&:persisted?)
render :index
end
end
def test
Ci::TestHookService.new.execute(hook, current_user)
redirect_to :back
end
def destroy
hook.destroy
redirect_to ci_project_web_hooks_path(@project)
end
private
def hook
@web_hook ||= @project.web_hooks.find(params[:id])
end
def project
@project = Ci::Project.find(params[:project_id])
end
def web_hook_params
params.require(:web_hook).permit(:url)
end
end
end
@@ -1,6 +1,6 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include PageLayoutHelper
include Gitlab::PageLayoutHelper
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
@@ -1,5 +1,5 @@
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
include PageLayoutHelper
include Gitlab::PageLayoutHelper
layout 'profile'
@@ -1,6 +1,6 @@
class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
include ApplicationHelper
include Gitlab::ApplicationHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
+1 -1
View File
@@ -1,6 +1,6 @@
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
include Gitlab::TreeHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
+1 -1
View File
@@ -5,7 +5,7 @@ class Projects::WikisController < Projects::ApplicationController
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki
include WikiHelper
include Gitlab::WikiHelper
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
+1 -1
View File
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
include SearchHelper
include Gitlab::SearchHelper
layout 'search'
-21
View File
@@ -1,21 +0,0 @@
module AppearancesHelper
def brand_item
nil
end
def brand_title
'GitLab Community Edition'
end
def brand_image
nil
end
def brand_text
nil
end
def brand_header_logo
image_tag 'logo.svg'
end
end
-315
View File
@@ -1,315 +0,0 @@
require 'digest/md5'
require 'uri'
module ApplicationHelper
# Check if a particular controller is the current one
#
# args - One or more controller names to check
#
# Examples
#
# # On TreeController
# current_controller?(:tree) # => true
# current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true
def current_controller?(*args)
args.any? { |v| v.to_s.downcase == controller.controller_name }
end
# Check if a particular action is the current one
#
# args - One or more action names to check
#
# Examples
#
# # On Projects#new
# current_action?(:new) # => true
# current_action?(:create) # => false
# current_action?(:new, :create) # => true
def current_action?(*args)
args.any? { |v| v.to_s.downcase == action_name }
end
def project_icon(project_id, options = {})
project =
if project_id.is_a?(Project)
project = project_id
else
Project.find_with_namespace(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
end
def avatar_icon(user_email = '', size = nil)
user = User.find_by(email: user_email)
if user
user.avatar_url(size) || default_avatar
else
gravatar_icon(user_email, size)
end
end
def gravatar_icon(user_email = '', size = nil)
GravatarService.new.execute(user_email, size) ||
default_avatar
end
def default_avatar
image_path('no_avatar.png')
end
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
else
'Never'
end
rescue
'Never'
end
def grouped_options_refs
repository = @project.repository
options = [
['Branches', repository.branch_names],
['Tags', VersionSorter.rsort(repository.tag_names)]
]
# If reference is commit id - we should add it to branch/tag selectbox
if(@ref && !options.flatten.include?(@ref) &&
@ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
options << ['Commit', [@ref]]
end
grouped_options_for_select(options, @ref || @project.default_branch)
end
def emoji_autocomplete_source
# should be an array of strings
# so to_s can be called, because it is sufficient and to_json is too slow
Emoji.names.to_s
end
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
# Skip if event is not about added or modified non-master branch
return false unless event && event.last_push_to_non_root? && !event.rm_ref?
project = event.project
# Skip if project repo is empty or MR disabled
return false unless project && !project.empty_repo? && project.merge_requests_enabled
# Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
return false unless project.repository.branch_names.include?(event.branch_name)
true
end
def hexdigest(string)
Digest::SHA1.hexdigest string
end
def simple_sanitize(str)
sanitize(str, tags: %w(a span))
end
def body_data_page
path = controller.controller_path.split('/')
namespace = path.first if path.second
[namespace, controller.controller_name, controller.action_name].compact.join(':')
end
# shortcut for gitlab config
def gitlab_config
Gitlab.config.gitlab
end
# shortcut for gitlab extra config
def extra_config
Gitlab.config.extra
end
def search_placeholder
if @project && @project.persisted?
'Search in this project'
elsif @snippet || @snippets || @show_snippets
'Search snippets'
elsif @group && @group.persisted?
'Search in this group'
else
'Search'
end
end
def broadcast_message
BroadcastMessage.current
end
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
# skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
# times, for example rendering hundreds of commits, it's advisable to disable
# this behavior using the `skip_js` argument and re-initializing `timeago`
# manually once all of the elements have been rendered.
#
# A `js-timeago` class is always added to the element, even when a custom
# `html_class` argument is provided.
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago",
datetime: time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
element
end
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.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
GitHub::Markup.render(file_name, file_content).
force_encoding(file_content.encoding).html_safe
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
'about.gitlab.com'
end
def promo_url
'https://' + promo_host
end
def page_filter_path(options = {})
without = options.delete(:without)
exist_opts = {
state: params[:state],
scope: params[:scope],
label_name: params[:label_name],
milestone_id: params[:milestone_id],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
}
options = exist_opts.merge(options)
if without.present?
without.each do |key|
options.delete(key)
end
end
path = request.path
path << "?#{options.to_param}"
path
end
def outdated_browser?
browser.ie? && browser.version.to_i < 10
end
def path_to_key(key, admin = false)
if admin
admin_user_key_path(@user, key)
else
profile_key_path(key)
end
end
def state_filters_text_for(entity, project)
titles = {
opened: "Open"
}
entity_title = titles[entity] || entity.to_s.humanize
count =
if project.nil?
nil
elsif current_controller?(:issues)
project.issues.send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
html = content_tag :span, entity_title
if count.present?
html += " "
html += content_tag :span, number_with_delimiter(count), class: 'badge'
end
html.html_safe
end
end
@@ -1,59 +0,0 @@
module ApplicationSettingsHelper
def gravatar_enabled?
current_application_settings.gravatar_enabled?
end
def twitter_sharing_enabled?
current_application_settings.twitter_sharing_enabled?
end
def signup_enabled?
current_application_settings.signup_enabled?
end
def signin_enabled?
current_application_settings.signin_enabled?
end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level)
css_class = 'btn'
css_class += ' active' if checked
checkbox_name = 'application_setting[restricted_visibility_levels][]'
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id) + name
end
end
end
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
css_class = 'btn'
css_class += ' active' if checked
checkbox_name = 'application_setting[import_sources][]'
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id) + name
end
end
end
end
-50
View File
@@ -1,50 +0,0 @@
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
end
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
def auth_providers
Gitlab::OAuth::Provider.providers
end
def label_for_provider(name)
Gitlab::OAuth::Provider.label_for(name)
end
def form_based_provider?(name)
FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
end
def form_based_providers
auth_providers.select { |provider| form_based_provider?(provider) }
end
def button_based_providers
auth_providers.reject { |provider| form_based_provider?(provider) }
end
def provider_image_tag(provider, size = 64)
label = label_for_provider(provider)
if provider_has_icon?(provider)
file_name = "#{provider.to_s.split('_').first}_#{size}.png"
image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}")
else
label
end
end
def auth_active?(provider)
current_user.identities.exists?(provider: provider.to_s)
end
extend self
end
-74
View File
@@ -1,74 +0,0 @@
module BlobHelper
def highlight(blob_name, blob_content, nowrap: false, continue: false)
@formatter ||= Rouge::Formatters::HTMLGitlab.new(
nowrap: nowrap,
cssclass: 'code highlight',
lineanchors: true,
lineanchorsid: 'LC'
)
begin
@lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
rescue
@lexer = Rouge::Lexers::PlainText
result = @formatter.format(@lexer.lex(blob_content)).html_safe
end
result
end
def no_highlight_files
%w(credits changelog news copying copyright license authors)
end
def edit_blob_link(project, ref, path, options = {})
blob =
begin
project.repository.blob_at(ref, path)
rescue
nil
end
if blob && blob.text?
text = 'Edit'
after = options[:after] || ''
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
cls = 'btn btn-small'
if allowed_tree_edit?(project, ref)
link_to(text,
namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts),
class: cls
)
else
content_tag :span, text, class: cls + ' disabled'
end + after.html_safe
else
''
end
end
def leave_edit_message
"Leave edit mode?\nAll unsaved changes will be lost."
end
def editing_preview_title(filename)
if Gitlab::MarkupHelper.previewable?(filename)
'Preview'
else
'Preview changes'
end
end
# Return an image icon depending on the file mode and extension
#
# mode - File unix mode
# mode - File name
def blob_icon(mode, name)
icon("#{file_type_icon_class('file', mode, name)} fw")
end
end
-17
View File
@@ -1,17 +0,0 @@
module BranchesHelper
def can_remove_branch?(project, branch_name)
if project.protected_branch? branch_name
false
elsif branch_name == project.repository.root_ref
false
else
can?(current_user, :push_code, project)
end
end
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
end
-16
View File
@@ -1,16 +0,0 @@
module BroadcastMessagesHelper
def broadcast_styling(broadcast_message)
styling = ''
if broadcast_message.color.present?
styling << "background-color: #{broadcast_message.color}"
styling << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
styling << "color: #{broadcast_message.font}"
end
styling
end
end
+140
View File
@@ -0,0 +1,140 @@
module Ci
module ApplicationHelper
def loader_html
image_tag 'ci/loader.gif', alt: 'Loading'
end
# Navigation link helper
#
# Returns an `li` element with an 'active' class if the supplied
# controller(s) and/or action(s) are currently active. The content of the
# element is the value passed to the block.
#
# options - The options hash used to determine if the element is "active" (default: {})
# :controller - One or more controller names to check (optional).
# :action - One or more action names to check (optional).
# :path - A shorthand path, such as 'dashboard#index', to check (optional).
# :html_options - Extra options to be passed to the list element (optional).
# block - An optional block that will become the contents of the returned
# `li` element.
#
# When both :controller and :action are specified, BOTH must match in order
# to be marked as active. When only one is given, either can match.
#
# Examples
#
# # Assuming we're on TreeController#show
#
# # Controller matches, but action doesn't
# nav_link(controller: [:tree, :refs], action: :edit) { "Hello" }
# # => '<li>Hello</li>'
#
# # Controller matches
# nav_link(controller: [:tree, :refs]) { "Hello" }
# # => '<li class="active">Hello</li>'
#
# # Shorthand path
# nav_link(path: 'tree#show') { "Hello" }
# # => '<li class="active">Hello</li>'
#
# # Supplying custom options for the list element
# nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
# # => '<li class="home active">Hello</li>'
#
# Returns a list item element String
def nav_link(options = {}, &block)
if path = options.delete(:path)
if path.respond_to?(:each)
c = path.map { |p| p.split('#').first }
a = path.map { |p| p.split('#').last }
else
c, a, _ = path.split('#')
end
else
c = options.delete(:controller)
a = options.delete(:action)
end
if c && a
# When given both options, make sure BOTH are active
klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
else
# Otherwise check EITHER option
klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
end
# Add our custom class into the html_options, which may or may not exist
# and which may or may not already have a :class key
o = options.delete(:html_options) || {}
o[:class] ||= ''
o[:class] += ' ' + klass
o[:class].strip!
if block_given?
content_tag(:li, capture(&block), o)
else
content_tag(:li, nil, o)
end
end
# Check if a particular controller is the current one
#
# args - One or more controller names to check
#
# Examples
#
# # On TreeController
# current_controller?(:tree) # => true
# current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true
def current_controller?(*args)
args.any? { |v| v.to_s.downcase == controller.controller_name }
end
# Check if a particular action is the current one
#
# args - One or more action names to check
#
# Examples
#
# # On Projects#new
# current_action?(:new) # => true
# current_action?(:create) # => false
# current_action?(:new, :create) # => true
def current_action?(*args)
args.any? { |v| v.to_s.downcase == action_name }
end
def date_from_to(from, to)
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
def body_data_page
path = controller.controller_path.split('/')
namespace = path.first if path.second
[namespace, controller.controller_name, controller.action_name].compact.join(":")
end
def duration_in_words(finished_at, started_at)
if finished_at && started_at
interval_in_seconds = finished_at.to_i - started_at.to_i
elsif started_at
interval_in_seconds = Time.now.to_i - started_at.to_i
end
time_interval_in_words(interval_in_seconds)
end
def time_interval_in_words(interval_in_seconds)
minutes = interval_in_seconds / 60
seconds = interval_in_seconds - minutes * 60
if minutes >= 1
"#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
else
"#{pluralize(seconds, "second")}"
end
end
end
end
+41
View File
@@ -0,0 +1,41 @@
module Ci
module BuildsHelper
def build_ref_link build
gitlab_ref_link build.project, build.ref
end
def build_compare_link build
gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha
end
def build_commit_link build
gitlab_commit_link build.project, build.short_sha
end
def build_url(build)
ci_project_build_url(build.project, build)
end
def build_status_alert_class(build)
if build.success?
'alert-success'
elsif build.failed?
'alert-danger'
elsif build.canceled?
'alert-disabled'
else
'alert-warning'
end
end
def build_icon_css_class(build)
if build.success?
'fa-circle cgreen'
elsif build.failed?
'fa-circle cred'
else
'fa-circle light'
end
end
end
end
+26
View File
@@ -0,0 +1,26 @@
module Ci
module CommitsHelper
def commit_status_alert_class(commit)
return unless commit
case commit.status
when 'success'
'alert-success'
when 'failed', 'canceled'
'alert-danger'
when 'skipped'
'alert-disabled'
else
'alert-warning'
end
end
def commit_link(commit)
link_to(commit.short_sha, ci_project_ref_commit_path(commit.project, commit.ref, commit.sha))
end
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
end
end
+36
View File
@@ -0,0 +1,36 @@
module Ci
module GitlabHelper
def no_turbolink
{ :"data-no-turbolink" => "data-no-turbolink" }
end
def gitlab_ref_link project, ref
gitlab_url = project.gitlab_url.dup
gitlab_url << "/commits/#{ref}"
link_to ref, gitlab_url, no_turbolink
end
def gitlab_compare_link project, before, after
gitlab_url = project.gitlab_url.dup
gitlab_url << "/compare/#{before}...#{after}"
link_to "#{before}...#{after}", gitlab_url, no_turbolink
end
def gitlab_commit_link project, sha
gitlab_url = project.gitlab_url.dup
gitlab_url << "/commit/#{sha}"
link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink
end
def yaml_web_editor_link(project)
commits = project.commits
if commits.any? && commits.last.push_data[:ci_yaml_file]
"#{@project.gitlab_url}/edit/master/.gitlab-ci.yml"
else
"#{@project.gitlab_url}/new/master"
end
end
end
end
+11
View File
@@ -0,0 +1,11 @@
module Ci
module IconsHelper
def boolean_to_icon(value)
if value.to_s == "true"
content_tag :i, nil, class: 'fa-circle cgreen'
else
content_tag :i, nil, class: 'fa-power-off clgray'
end
end
end
end
+36
View File
@@ -0,0 +1,36 @@
module Ci
module ProjectsHelper
def ref_tab_class ref = nil
'active' if ref == @ref
end
def success_ratio(success_builds, failed_builds)
failed_builds = failed_builds.count(:all)
success_builds = success_builds.count(:all)
return 100 if failed_builds.zero?
ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
ratio.to_i
end
def markdown_badge_code(project, ref)
url = status_ci_project_url(project, ref: ref, format: 'png')
"[![build status](#{url})](#{ci_project_url(project, ref: ref)})"
end
def html_badge_code(project, ref)
url = status_ci_project_url(project, ref: ref, format: 'png')
"<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>"
end
def project_uses_specific_runner?(project)
project.runners.any?
end
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
end
end
end
+29
View File
@@ -0,0 +1,29 @@
module Ci
module RoutesHelper
class Base
include Gitlab::Application.routes.url_helpers
def default_url_options
{
host: Ci::Settings.gitlab_ci['host'],
protocol: Ci::Settings.gitlab_ci['https'] ? "https" : "http",
port: Ci::Settings.gitlab_ci['port']
}
end
end
def url_helpers
@url_helpers ||= Ci::Base.new
end
def self.method_missing(method, *args, &block)
@url_helpers ||= Ci::Base.new
if @url_helpers.respond_to?(method)
@url_helpers.send(method, *args, &block)
else
super method, *args, &block
end
end
end
end
+22
View File
@@ -0,0 +1,22 @@
module Ci
module RunnersHelper
def runner_status_icon(runner)
unless runner.contacted_at
return content_tag :i, nil,
class: "fa-warning-sign",
title: "New runner. Has not connected yet"
end
status =
if runner.active?
runner.contacted_at > 3.hour.ago ? :online : :offline
else
:paused
end
content_tag :i, nil,
class: "fa-circle runner-status-#{status}",
title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
end
end
end
+7
View File
@@ -0,0 +1,7 @@
module Ci
module TriggersHelper
def build_trigger_url(project_id, ref_name)
"#{Ci::Settings.gitlab_ci.url}/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
end
end
end
+15
View File
@@ -0,0 +1,15 @@
module Ci
module UserHelper
def user_avatar_url(user = nil, size = nil, default = 'identicon')
size = 40 if size.nil? || size <= 0
if user.blank? || user.avatar_url.blank?
'ci/no_avatar.png'
elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url
Regexp.last_match[0] + "?s=#{size}&d=#{default}"
else
user.avatar_url
end
end
end
end
+32
View File
@@ -0,0 +1,32 @@
module Ci
module UserSessionsHelper
def generate_oauth_salt
SecureRandom.hex(16)
end
def generate_oauth_hmac(salt, return_to)
return unless return_to
digest = OpenSSL::Digest.new('sha256')
key = Gitlab::Application.secrets.db_key_base + salt
OpenSSL::HMAC.hexdigest(digest, key, return_to)
end
def generate_oauth_state(return_to)
return unless return_to
salt = generate_oauth_salt
hmac = generate_oauth_hmac(salt, return_to)
"#{salt}:#{hmac}:#{return_to}"
end
def get_ouath_state_return_to(state)
state.split(':', 3)[2] if state
end
def is_oauth_state_valid?(state)
return true unless state
salt, hmac, return_to = state.split(':', 3)
return false unless return_to
hmac == generate_oauth_hmac(salt, return_to)
end
end
end
-183
View File
@@ -1,183 +0,0 @@
# encoding: utf-8
module CommitsHelper
# Returns a link to the commit author. If the author has a matching user and
# is a member of the current @project it will link to the team member page.
# Otherwise it will link to the author email as specified in the commit.
#
# options:
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_author_link(commit, options = {})
commit_person_link(commit, options.merge(source: :author))
end
# Just like #author_link but for the committer.
def commit_committer_link(commit, options = {})
commit_person_link(commit, options.merge(source: :committer))
end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
elsif diff.new_file
"added"
else
nil
end
end
def commit_to_html(commit, project, inline = true)
template = inline ? "inline_commit" : "commit"
escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil?
end
# Breadcrumb links for a Project and, if applicable, a tree path
def commits_breadcrumbs
return unless @project && @ref
# Add the root project link and the arrow icon
crumbs = content_tag(:li) do
link_to(
@project.path,
namespace_project_commits_path(@project.namespace, @project, @ref)
)
end
if @path
parts = @path.split('/')
parts.each_with_index do |part, i|
crumbs << content_tag(:li) do
# The text is just the individual part, but the link needs all the parts before it
link_to(
part,
namespace_project_commits_path(
@project.namespace,
@project,
tree_join(@ref, parts[0..i].join('/'))
)
)
end
end
end
crumbs.html_safe
end
# Return Project default branch, if it present in array
# Else - first branch in array (mb last actual branch)
def commit_default_branch(project, branches)
branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
end
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
link_to(
namespace_project_tree_path(project.namespace, project, branch)
) do
content_tag :span, class: 'label label-gray' do
icon('code-fork') + ' ' + branch
end
end
end.join(" ").html_safe
end
# Returns the sorted links to tags, separated by a comma
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
link_to(
namespace_project_commits_path(project.namespace, project,
project.repository.find_tag(tag).name)
) do
content_tag :span, class: 'label label-gray' do
icon('tag') + ' ' + tag
end
end
end.join(" ").html_safe
end
def link_to_browse_code(project, commit)
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
"Browse File »",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
)
elsif @path.present?
return link_to(
"Browse Dir »",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
)
end
end
link_to(
"Browse Code »",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
)
end
protected
# Private: Returns a link to a person. If the person has a matching user and
# is a member of the current @project it will link to the team member page.
# Otherwise it will link to the person email as specified in the commit.
#
# options:
# source: one of :author or :committer
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_person_link(commit, options = {})
user = commit.send(options[:source])
source_name = clean(commit.send "#{options[:source]}_name".to_sym)
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text =
if options[:avatar]
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
options = {
class: "commit-#{options[:source]}-link has_tooltip",
data: { :'original-title' => sanitize(source_email) }
}
if user.nil?
mail_to(source_email, text.html_safe, options)
else
link_to(text.html_safe, user_path(user), options)
end
end
def view_file_btn(commit_sha, diff, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)),
class: 'btn btn-small view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
end
end
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
end
-21
View File
@@ -1,21 +0,0 @@
module CompareHelper
def create_mr_button?(from = params[:from], to = params[:to], project = @project)
from.present? &&
to.present? &&
from != to &&
project.merge_requests_enabled &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
def create_mr_path(from = params[:from], to = params[:to], project = @project)
new_namespace_project_merge_request_path(
project.namespace,
project,
merge_request: {
source_branch: to,
target_branch: from
}
)
end
end
-9
View File
@@ -1,9 +0,0 @@
module DashboardHelper
def assigned_issues_dashboard_path
issues_dashboard_path(assignee_id: current_user.id)
end
def assigned_mrs_dashboard_path
merge_requests_dashboard_path(assignee_id: current_user.id)
end
end
-170
View File
@@ -1,170 +0,0 @@
module DiffHelper
def allowed_diff_size
if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_FILES
else
Commit::DIFF_SAFE_FILES
end
end
def allowed_diff_lines
if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_LINES
else
Commit::DIFF_SAFE_LINES
end
end
def safe_diff_files(diffs)
lines = 0
safe_files = []
diffs.first(allowed_diff_size).each do |diff|
lines += diff.diff.lines.count
break if lines > allowed_diff_lines
safe_files << Gitlab::Diff::File.new(diff)
end
safe_files
end
def diff_hard_limit_enabled?
# Enabling hard limit allows user to see more diff information
if params[:force_show_diff].present?
true
else
false
end
end
def generate_line_code(file_path, line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
def parallel_diff(diff_file, index)
lines = []
skip_next = false
# Building array of lines
#
# [
# left_type, left_line_number, left_line_content, left_line_code,
# right_line_type, right_line_number, right_line_content, right_line_code
# ]
#
diff_file.diff_lines.each do |line|
full_line = line.text
type = line.type
line_code = generate_line_code(diff_file.file_path, line)
line_new = line.new_pos
line_old = line.old_pos
next_line = diff_file.next_line(line.index)
if next_line
next_line_code = generate_line_code(diff_file.file_path, next_line)
next_type = next_line.type
next_line = next_line.text
end
if type == 'match' || type.nil?
# line in the right panel is the same as in the left one
line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code]
lines.push(line)
elsif type == 'old'
if next_type == 'new'
# Left side has text removed, right side has text added
line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code]
lines.push(line)
skip_next = true
elsif next_type == 'old' || next_type.nil?
# Left side has text removed, right side doesn't have any change
# No next line code, no new line number, no new line text
line = [type, line_old, full_line, line_code, next_type, nil, "&nbsp;", nil]
lines.push(line)
end
elsif type == 'new'
if skip_next
# Change has been already included in previous line so no need to do it again
skip_next = false
next
else
# Change is only on the right side, left side has no change
line = [nil, nil, "&nbsp;", line_code, type, line_new, full_line, line_code]
lines.push(line)
end
end
end
lines
end
def unfold_bottom_class(bottom)
(bottom) ? 'js-unfold-bottom' : ''
end
def unfold_class(unfold)
(unfold) ? 'unfold js-unfold' : ''
end
def diff_line_content(line)
if line.blank?
" &nbsp;"
else
line
end
end
def line_comments
@line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
end
def organize_comments(type_left, type_right, line_code_left, line_code_right)
comments_left = comments_right = nil
unless type_left.nil? && type_right == 'new'
comments_left = line_comments[line_code_left]
end
unless type_left.nil? && type_right.nil?
comments_right = line_comments[line_code_right]
end
[comments_left, comments_right]
end
def inline_diff_btn
params_copy = params.dup
params_copy[:view] = 'inline'
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
'Inline'
end
end
def parallel_diff_btn
params_copy = params.dup
params_copy[:view] = 'parallel'
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
'Side-by-side'
end
end
def submodule_link(blob, ref, repository = @repository)
tree, commit = submodule_links(blob, ref, repository)
commit_id = if commit.nil?
blob.id[0..10]
else
link_to "#{blob.id[0..10]}", commit
end
[
content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
'@',
content_tag(:span, commit_id, class: 'monospace'),
].join(' ').html_safe
end
end
-57
View File
@@ -1,57 +0,0 @@
module EmailsHelper
# Google Actions
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
name = action_title(url)
if name
data = {
"@context" => "http://schema.org",
"@type" => "EmailMessage",
"action" => {
"@type" => "ViewAction",
"name" => name,
"url" => url,
}
}
content_tag :script, type: 'application/ld+json' do
data.to_json.html_safe
end
end
end
def action_title(url)
return unless url
["merge_requests", "issues", "commit"].each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
end
end
end
def color_email_diff(diffcontent)
formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
lexer = Rouge::Lexers::Diff
raw formatter.format(lexer.lex(diffcontent))
end
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
unit = 'day'
valid_length = (valid_hours / 24).floor
else
unit = 'hour'
valid_length = valid_hours.floor
end
pluralize(valid_length, unit)
end
def reset_token_expire_message
link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email))
msg = "This link is valid for #{password_reset_token_valid_time}. "
msg << "After it expires, you can #{link_tag}."
end
end
-203
View File
@@ -1,203 +0,0 @@
module EventsHelper
def link_to_author(event)
author = event.author
if author
link_to author.name, user_path(author.username)
else
event.author_name
end
end
def event_action_name(event)
target = if event.target_type
if event.note?
event.note_target_type
else
event.target_type.titleize.downcase
end
else
'project'
end
[event.action_name, target].join(" ")
end
def event_filter_link(key, tooltip)
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
class: 'event_filter_link',
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
data: { toggle: 'tooltip', placement: 'top' }
}
content_tag :li, class: "filter_icon #{active}" do
link_to request.path, link_opts do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
end
def icon_for_event
{
EventFilter.push => 'upload',
EventFilter.merged => 'check-square-o',
EventFilter.comments => 'comments',
EventFilter.team => 'user',
}
end
def event_feed_title(event)
words = []
words << event.author_name
words << event_action_name(event)
if event.push?
words << event.ref_type
words << event.ref_name
words << "at"
elsif event.commented?
if event.note_commit?
words << event.note_short_commit_id
else
words << "##{truncate event.note_target_iid}"
end
words << "at"
elsif event.target
words << "##{event.target_iid}:"
words << event.target.title if event.target.respond_to?(:title)
words << "at"
end
words << event.project_name
words.join(" ")
end
def event_feed_url(event)
if event.issue?
namespace_project_issue_url(event.project.namespace, event.project,
event.issue)
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
elsif event.note? && event.note_commit?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
if event.note_target
if event.note_commit?
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
anchor: dom_id(event.target))
elsif event.note_project_snippet?
namespace_project_snippet_path(event.project.namespace,
event.project, event.note_target)
else
event_note_target_path(event)
end
end
elsif event.push?
if event.push_with_commits? && event.md_ref?
if event.commits_count > 1
namespace_project_compare_url(event.project.namespace, event.project,
from: event.commit_from, to:
event.commit_to)
else
namespace_project_commit_url(event.project.namespace, event.project,
id: event.commit_to)
end
else
namespace_project_commits_url(event.project.namespace, event.project,
event.ref_name)
end
end
end
def event_feed_summary(event)
if event.issue?
render "events/event_issue", issue: event.issue
elsif event.push?
render "events/event_push", event: event
elsif event.merge_request?
render "events/event_merge_request", merge_request: event.merge_request
elsif event.note?
render "events/event_note", note: event.note
end
end
def event_note_target_path(event)
if event.note? && event.note_commit?
namespace_project_commit_path(event.project.namespace, event.project,
event.note_target)
else
polymorphic_path([event.project.namespace.becomes(Namespace),
event.project, event.note_target],
anchor: dom_id(event.target))
end
end
def event_note_title_html(event)
if event.note_target
if event.note_commit?
link_to(
namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id,
anchor: dom_id(event.target)),
class: "commit_short_id"
) do
"#{event.note_target_type} #{event.note_short_commit_id}"
end
elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace,
event.project,
event.note_target)) do
"#{event.note_target_type} ##{truncate event.note_target_id}"
end
else
link_to event_note_target_path(event) do
"#{event.note_target_type} ##{truncate event.note_target_iid}"
end
end
else
content_tag :strong do
"(deleted)"
end
end
end
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
sanitize(text, tags: %w(a img b pre code p span))
end
def event_commit_title(message)
escape_once(truncate(message.split("\n").first, length: 70))
rescue
"--broken encoding"
end
def event_to_atom(xml, event)
if event.proper?
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
event_summary = event_feed_summary(event)
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_link
xml.title truncate(event_title, length: 80)
xml.updated event.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email)
xml.author do |author|
xml.name event.author_name
xml.email event.author_email
end
xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? }
end
end
end
end
-17
View File
@@ -1,17 +0,0 @@
module ExploreHelper
def explore_projects_filter_path(options={})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
group: params[:group],
tag: params[:tag],
visibility_level: params[:visibility_level],
}
options = exist_opts.merge(options)
path = explore_projects_path
path << "?#{options.to_param}"
path
end
end
-11
View File
@@ -1,11 +0,0 @@
module ExternalWikiHelper
def get_project_wiki_path(project)
external_wiki_service = project.services.
select { |service| service.to_param == 'external_wiki' }.first
if external_wiki_service.present? && external_wiki_service.active?
external_wiki_service.properties['external_wiki_url']
else
namespace_project_wiki_path(project.namespace, project, :home)
end
end
end
-5
View File
@@ -1,5 +0,0 @@
module GitHelper
def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
end
end
+23
View File
@@ -0,0 +1,23 @@
module Gitlab
module AppearancesHelper
def brand_item
nil
end
def brand_title
'GitLab Community Edition'
end
def brand_image
nil
end
def brand_text
nil
end
def brand_header_logo
image_tag 'logo.svg'
end
end
end
+317
View File
@@ -0,0 +1,317 @@
require 'digest/md5'
require 'uri'
module Gitlab
module ApplicationHelper
# Check if a particular controller is the current one
#
# args - One or more controller names to check
#
# Examples
#
# # On TreeController
# current_controller?(:tree) # => true
# current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true
def current_controller?(*args)
args.any? { |v| v.to_s.downcase == controller.controller_name }
end
# Check if a particular action is the current one
#
# args - One or more action names to check
#
# Examples
#
# # On Projects#new
# current_action?(:new) # => true
# current_action?(:create) # => false
# current_action?(:new, :create) # => true
def current_action?(*args)
args.any? { |v| v.to_s.downcase == action_name }
end
def project_icon(project_id, options = {})
project =
if project_id.is_a?(Project)
project = project_id
else
Project.find_with_namespace(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
end
def avatar_icon(user_email = '', size = nil)
user = User.find_by(email: user_email)
if user
user.avatar_url(size) || default_avatar
else
gravatar_icon(user_email, size)
end
end
def gravatar_icon(user_email = '', size = nil)
GravatarService.new.execute(user_email, size) ||
default_avatar
end
def default_avatar
image_path('no_avatar.png')
end
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
else
'Never'
end
rescue
'Never'
end
def grouped_options_refs
repository = @project.repository
options = [
['Branches', repository.branch_names],
['Tags', VersionSorter.rsort(repository.tag_names)]
]
# If reference is commit id - we should add it to branch/tag selectbox
if(@ref && !options.flatten.include?(@ref) &&
@ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
options << ['Commit', [@ref]]
end
grouped_options_for_select(options, @ref || @project.default_branch)
end
def emoji_autocomplete_source
# should be an array of strings
# so to_s can be called, because it is sufficient and to_json is too slow
Emoji.names.to_s
end
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
# Skip if event is not about added or modified non-master branch
return false unless event && event.last_push_to_non_root? && !event.rm_ref?
project = event.project
# Skip if project repo is empty or MR disabled
return false unless project && !project.empty_repo? && project.merge_requests_enabled
# Skip if user already created appropriate MR
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
return false unless project.repository.branch_names.include?(event.branch_name)
true
end
def hexdigest(string)
Digest::SHA1.hexdigest string
end
def simple_sanitize(str)
sanitize(str, tags: %w(a span))
end
def body_data_page
path = controller.controller_path.split('/')
namespace = path.first if path.second
[namespace, controller.controller_name, controller.action_name].compact.join(':')
end
# shortcut for gitlab config
def gitlab_config
Gitlab.config.gitlab
end
# shortcut for gitlab extra config
def extra_config
Gitlab.config.extra
end
def search_placeholder
if @project && @project.persisted?
'Search in this project'
elsif @snippet || @snippets || @show_snippets
'Search snippets'
elsif @group && @group.persisted?
'Search in this group'
else
'Search'
end
end
def broadcast_message
BroadcastMessage.current
end
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
# skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
# times, for example rendering hundreds of commits, it's advisable to disable
# this behavior using the `skip_js` argument and re-initializing `timeago`
# manually once all of the elements have been rendered.
#
# A `js-timeago` class is always added to the element, even when a custom
# `html_class` argument is provided.
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago",
datetime: time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
element
end
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Haml::Helpers.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
GitHub::Markup.render(file_name, file_content).
force_encoding(file_content.encoding).html_safe
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
'about.gitlab.com'
end
def promo_url
'https://' + promo_host
end
def page_filter_path(options = {})
without = options.delete(:without)
exist_opts = {
state: params[:state],
scope: params[:scope],
label_name: params[:label_name],
milestone_id: params[:milestone_id],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
}
options = exist_opts.merge(options)
if without.present?
without.each do |key|
options.delete(key)
end
end
path = request.path
path << "?#{options.to_param}"
path
end
def outdated_browser?
browser.ie? && browser.version.to_i < 10
end
def path_to_key(key, admin = false)
if admin
admin_user_key_path(@user, key)
else
profile_key_path(key)
end
end
def state_filters_text_for(entity, project)
titles = {
opened: "Open"
}
entity_title = titles[entity] || entity.to_s.humanize
count =
if project.nil?
nil
elsif current_controller?(:issues)
project.issues.send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
html = content_tag :span, entity_title
if count.present?
html += " "
html += content_tag :span, number_with_delimiter(count), class: 'badge'
end
html.html_safe
end
end
end
@@ -0,0 +1,61 @@
module Gitlab
module ApplicationSettingsHelper
def gravatar_enabled?
current_application_settings.gravatar_enabled?
end
def twitter_sharing_enabled?
current_application_settings.twitter_sharing_enabled?
end
def signup_enabled?
current_application_settings.signup_enabled?
end
def signin_enabled?
current_application_settings.signin_enabled?
end
def extra_sign_in_text
current_application_settings.sign_in_text
end
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level)
css_class = 'btn'
css_class += ' active' if checked
checkbox_name = 'application_setting[restricted_visibility_levels][]'
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id) + name
end
end
end
# Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect.
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
css_class = 'btn'
css_class += ' active' if checked
checkbox_name = 'application_setting[import_sources][]'
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id) + name
end
end
end
end
end
+52
View File
@@ -0,0 +1,52 @@
module Gitlab
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
end
def provider_has_icon?(name)
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
def auth_providers
Gitlab::OAuth::Provider.providers
end
def label_for_provider(name)
Gitlab::OAuth::Provider.label_for(name)
end
def form_based_provider?(name)
FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
end
def form_based_providers
auth_providers.select { |provider| form_based_provider?(provider) }
end
def button_based_providers
auth_providers.reject { |provider| form_based_provider?(provider) }
end
def provider_image_tag(provider, size = 64)
label = label_for_provider(provider)
if provider_has_icon?(provider)
file_name = "#{provider.to_s.split('_').first}_#{size}.png"
image_tag("auth_buttons/#{file_name}", alt: label, title: "Sign in with #{label}")
else
label
end
end
def auth_active?(provider)
current_user.identities.exists?(provider: provider.to_s)
end
extend self
end
end
+76
View File
@@ -0,0 +1,76 @@
module Gitlab
module BlobHelper
def highlight(blob_name, blob_content, nowrap: false, continue: false)
@formatter ||= Rouge::Formatters::HTMLGitlab.new(
nowrap: nowrap,
cssclass: 'code highlight',
lineanchors: true,
lineanchorsid: 'LC'
)
begin
@lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
rescue
@lexer = Rouge::Lexers::PlainText
result = @formatter.format(@lexer.lex(blob_content)).html_safe
end
result
end
def no_highlight_files
%w(credits changelog news copying copyright license authors)
end
def edit_blob_link(project, ref, path, options = {})
blob =
begin
project.repository.blob_at(ref, path)
rescue
nil
end
if blob && blob.text?
text = 'Edit'
after = options[:after] || ''
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
cls = 'btn btn-small'
if allowed_tree_edit?(project, ref)
link_to(text,
namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts),
class: cls
)
else
content_tag :span, text, class: cls + ' disabled'
end + after.html_safe
else
''
end
end
def leave_edit_message
"Leave edit mode?\nAll unsaved changes will be lost."
end
def editing_preview_title(filename)
if Gitlab::MarkupHelper.previewable?(filename)
'Preview'
else
'Preview changes'
end
end
# Return an image icon depending on the file mode and extension
#
# mode - File unix mode
# mode - File name
def blob_icon(mode, name)
icon("#{file_type_icon_class('file', mode, name)} fw")
end
end
end
+19
View File
@@ -0,0 +1,19 @@
module Gitlab
module BranchesHelper
def can_remove_branch?(project, branch_name)
if project.protected_branch? branch_name
false
elsif branch_name == project.repository.root_ref
false
else
can?(current_user, :push_code, project)
end
end
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
end
end
@@ -0,0 +1,18 @@
module Gitlab
module BroadcastMessagesHelper
def broadcast_styling(broadcast_message)
styling = ''
if broadcast_message.color.present?
styling << "background-color: #{broadcast_message.color}"
styling << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
styling << "color: #{broadcast_message.font}"
end
styling
end
end
end
+185
View File
@@ -0,0 +1,185 @@
# encoding: utf-8
module Gitlab
module CommitsHelper
# Returns a link to the commit author. If the author has a matching user and
# is a member of the current @project it will link to the team member page.
# Otherwise it will link to the author email as specified in the commit.
#
# options:
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_author_link(commit, options = {})
commit_person_link(commit, options.merge(source: :author))
end
# Just like #author_link but for the committer.
def commit_committer_link(commit, options = {})
commit_person_link(commit, options.merge(source: :committer))
end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
elsif diff.new_file
"added"
else
nil
end
end
def commit_to_html(commit, project, inline = true)
template = inline ? "inline_commit" : "commit"
escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil?
end
# Breadcrumb links for a Project and, if applicable, a tree path
def commits_breadcrumbs
return unless @project && @ref
# Add the root project link and the arrow icon
crumbs = content_tag(:li) do
link_to(
@project.path,
namespace_project_commits_path(@project.namespace, @project, @ref)
)
end
if @path
parts = @path.split('/')
parts.each_with_index do |part, i|
crumbs << content_tag(:li) do
# The text is just the individual part, but the link needs all the parts before it
link_to(
part,
namespace_project_commits_path(
@project.namespace,
@project,
tree_join(@ref, parts[0..i].join('/'))
)
)
end
end
end
crumbs.html_safe
end
# Return Project default branch, if it present in array
# Else - first branch in array (mb last actual branch)
def commit_default_branch(project, branches)
branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
end
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
link_to(
namespace_project_tree_path(project.namespace, project, branch)
) do
content_tag :span, class: 'label label-gray' do
icon('code-fork') + ' ' + branch
end
end
end.join(" ").html_safe
end
# Returns the sorted links to tags, separated by a comma
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
link_to(
namespace_project_commits_path(project.namespace, project,
project.repository.find_tag(tag).name)
) do
content_tag :span, class: 'label label-gray' do
icon('tag') + ' ' + tag
end
end
end.join(" ").html_safe
end
def link_to_browse_code(project, commit)
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
"Browse File »",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
)
elsif @path.present?
return link_to(
"Browse Dir »",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
)
end
end
link_to(
"Browse Code »",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
)
end
protected
# Private: Returns a link to a person. If the person has a matching user and
# is a member of the current @project it will link to the team member page.
# Otherwise it will link to the person email as specified in the commit.
#
# options:
# source: one of :author or :committer
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_person_link(commit, options = {})
user = commit.send(options[:source])
source_name = clean(commit.send "#{options[:source]}_name".to_sym)
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text =
if options[:avatar]
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
options = {
class: "commit-#{options[:source]}-link has_tooltip",
data: { :'original-title' => sanitize(source_email) }
}
if user.nil?
mail_to(source_email, text.html_safe, options)
else
link_to(text.html_safe, user_path(user), options)
end
end
def view_file_btn(commit_sha, diff, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)),
class: 'btn btn-small view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
end
end
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
def clean(string)
Sanitize.clean(string, remove_contents: true)
end
end
end
+23
View File
@@ -0,0 +1,23 @@
module Gitlab
module CompareHelper
def create_mr_button?(from = params[:from], to = params[:to], project = @project)
from.present? &&
to.present? &&
from != to &&
project.merge_requests_enabled &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
def create_mr_path(from = params[:from], to = params[:to], project = @project)
new_namespace_project_merge_request_path(
project.namespace,
project,
merge_request: {
source_branch: to,
target_branch: from
}
)
end
end
end

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