Commit cccd269d authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'ci-and-ce-sitting-in-a-tree-k-i-s-s-i-n-g' into 'master'

Merge CI into CE

First step of #2164.

- [x] Merge latest CE master
- [x] Make application start
- [x] Re-use gitlab sessions (remove CI oauth part)
- [x] Get rid of gitlab_ci.yml config
- [x] Make tests start
- [x] Make most CI features works
- [x] Make tests green
- [x] Write migration documentation
- [x] Add CI builds to CE backup

See merge request !1204
parents 7d59ba00 ac8d2eb0
...@@ -20,12 +20,13 @@ backups/* ...@@ -20,12 +20,13 @@ backups/*
config/aws.yml config/aws.yml
config/database.yml config/database.yml
config/gitlab.yml config/gitlab.yml
config/initializers/omniauth.rb config/gitlab_ci.yml
config/initializers/rack_attack.rb config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb config/initializers/smtp_settings.rb
config/resque.yml config/resque.yml
config/unicorn.rb config/unicorn.rb
config/mail_room.yml config/mail_room.yml
config/secrets.yml
coverage/* coverage/*
db/*.sqlite3 db/*.sqlite3
db/*.sqlite3-journal db/*.sqlite3-journal
...@@ -41,3 +42,4 @@ rails_best_practices_output.html ...@@ -41,3 +42,4 @@ rails_best_practices_output.html
/tags /tags
tmp/ tmp/
vendor/bundle/* vendor/bundle/*
builds/*
...@@ -998,7 +998,9 @@ AllCops: ...@@ -998,7 +998,9 @@ AllCops:
- 'tmp/**/*' - 'tmp/**/*'
- 'bin/**/*' - 'bin/**/*'
- 'lib/backup/**/*' - 'lib/backup/**/*'
- 'lib/ci/backup/**/*'
- 'lib/tasks/**/*' - 'lib/tasks/**/*'
- 'lib/ci/migrate/**/*'
- 'lib/email_validator.rb' - 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb' - 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb' - 'lib/gitlab/seeder.rb'
This diff is collapsed.
source "https://rubygems.org" 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 # Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY # See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
...@@ -10,29 +18,29 @@ gem 'sprockets', '~> 2.12.3' ...@@ -10,29 +18,29 @@ gem 'sprockets', '~> 2.12.3'
gem "default_value_for", "~> 3.0.0" gem "default_value_for", "~> 3.0.0"
# Supported DBs # Supported DBs
gem "mysql2", group: :mysql gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", group: :postgres gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries # Authentication libraries
gem "devise", '3.2.4' gem "devise", '~> 3.2.4'
gem "devise-async", '0.9.0' gem "devise-async", '~> 0.9.0'
gem 'omniauth', "~> 1.2.2" gem 'omniauth', "~> 1.2.2"
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2', '~> 0.2.5'
gem 'omniauth-twitter' gem 'omniauth-twitter', '~> 1.0.1'
gem 'omniauth-github' gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-shibboleth' gem 'omniauth-shibboleth', '~> 1.1.1'
gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos
gem 'omniauth-gitlab' gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-bitbucket' gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-saml', '~> 1.4.0' gem 'omniauth-saml', '~> 1.4.0'
gem 'doorkeeper', '~> 2.1.3'
gem 'omniauth_crowd' gem 'omniauth_crowd'
gem 'doorkeeper', '2.1.3'
gem "rack-oauth2", "~> 1.0.5" gem "rack-oauth2", "~> 1.0.5"
# Two-factor authentication # Two-factor authentication
gem 'devise-two-factor' gem 'devise-two-factor', '~> 1.0.1'
gem 'rqrcode-rails3' gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '1.3.4' gem 'attr_encrypted', '~> 1.3.4'
# Browser detection # Browser detection
gem "browser", '~> 1.0.0' gem "browser", '~> 1.0.0'
...@@ -44,7 +52,7 @@ gem "gitlab_git", '~> 7.2.15' ...@@ -44,7 +52,7 @@ gem "gitlab_git", '~> 7.2.15'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki # Git Wiki
gem 'gollum-lib', '~> 4.0.2' gem 'gollum-lib', '~> 4.0.2'
...@@ -59,47 +67,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist" ...@@ -59,47 +67,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
# API # API
gem "grape", "~> 0.6.1" gem "grape", "~> 0.6.1"
gem "grape-entity", "~> 0.4.2" 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 # Format dates and times
# based on human-friendly examples # based on human-friendly examples
gem "stamp" gem "stamp", '~> 0.5.0'
# Enumeration fields # Enumeration fields
gem 'enumerize' gem 'enumerize', '~> 0.7.0'
# Pagination # Pagination
gem "kaminari", "~> 0.15.1" gem "kaminari", "~> 0.15.1"
# HAML # HAML
gem "haml-rails" gem "haml-rails", '~> 0.5.3'
# Files attachments # Files attachments
gem "carrierwave" gem "carrierwave", '~> 0.9.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails' gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage # for aws storage
gem "fog", "~> 1.25.0" gem "fog", "~> 1.25.0"
gem "unf" gem "unf", '~> 0.1.4'
# Authorization # Authorization
gem "six" gem "six", '~> 0.2.0'
# Seed data # Seed data
gem "seed-fu" gem "seed-fu", '~> 2.3.5'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '1.0.2', require: 'task_list/railtie' gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'github-markup' gem 'github-markup', '~> 1.3.1'
gem 'redcarpet', '~> 3.3.2' gem 'redcarpet', '~> 3.3.2'
gem 'RedCloth' gem 'RedCloth', '~> 4.2.9'
gem 'rdoc', '~>3.6' gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~>0.3.6' gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
# Diffs # Diffs
...@@ -107,37 +115,38 @@ gem 'diffy', '~> 3.0.3' ...@@ -107,37 +115,38 @@ gem 'diffy', '~> 3.0.3'
# Application server # Application server
group :unicorn do group :unicorn do
gem "unicorn", '~> 4.6.3' gem "unicorn", '~> 4.8.2'
gem 'unicorn-worker-killer' gem 'unicorn-worker-killer', '~> 0.4.2'
end end
# State machine # State machine
gem "state_machine" gem "state_machine", '~> 1.2.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs # Background jobs
gem 'slim' gem 'slim', '~> 2.0.2'
gem 'sinatra', require: nil gem 'sinatra', '~> 1.4.4', require: nil
gem 'sidekiq', '~> 3.3' gem 'sidekiq', '3.3.0'
gem 'sidetiq', '0.6.3' gem 'sidetiq', '~> 0.6.3'
# HTTP requests # HTTP requests
gem "httparty" gem "httparty", '~> 0.13.3'
# Colored output to console # Colored output to console
gem "colored" gem "colored", '~> 1.2'
gem "colorize", '~> 0.5.8'
# GitLab settings # GitLab settings
gem 'settingslogic' gem 'settingslogic', '~> 2.0.9'
# Misc # Misc
gem "foreman"
gem 'version_sorter' gem 'version_sorter', '~> 2.0.0'
# Cache # Cache
gem "redis-rails" gem "redis-rails", '~> 4.0.0'
# Campfire integration # Campfire integration
gem 'tinder', '~> 1.9.2' gem 'tinder', '~> 1.9.2'
...@@ -176,69 +185,70 @@ gem "sanitize", '~> 2.0' ...@@ -176,69 +185,70 @@ gem "sanitize", '~> 2.0'
gem "rack-attack", '~> 4.3.0' gem "rack-attack", '~> 4.3.0'
# Ace editor # Ace editor
gem 'ace-rails-ap' gem 'ace-rails-ap', '~> 2.0.1'
# Keyboard shortcuts # Keyboard shortcuts
gem 'mousetrap-rails' gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes' gem 'charlock_holmes', '~> 0.6.9.4'
gem "sass-rails", '~> 4.0.5' gem "sass-rails", '~> 4.0.5'
gem "coffee-rails" gem "coffee-rails", '~> 4.1.0'
gem "uglifier" gem "uglifier", '~> 2.3.2'
gem 'turbolinks', '~> 2.5.0' 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 'bootstrap-sass', '~> 3.0'
gem 'font-awesome-rails', '~> 4.2' gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.1' gem 'gitlab_emoji', '~> 0.1'
gem 'gon', '~> 5.0.0' gem 'gon', '~> 5.0.0'
gem 'jquery-atwho-rails', '~> 1.0.0' gem 'jquery-atwho-rails', '~> 1.0.0'
gem 'jquery-rails', '3.1.3' gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-scrollto-rails' gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails' gem 'jquery-ui-rails', '~> 4.2.1'
gem 'nprogress-rails' gem 'nprogress-rails', '~> 0.1.2.3'
gem 'raphael-rails', '~> 2.1.2' gem 'raphael-rails', '~> 2.1.2'
gem 'request_store' gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus' gem 'virtus', '~> 1.0.1'
group :development do group :development do
gem 'brakeman', require: false gem "foreman"
gem "annotate", "~> 2.6.0.beta2" gem 'brakeman', '3.0.1', require: false
gem "letter_opener"
gem 'quiet_assets', '~> 1.0.1' gem "annotate", "~> 2.6.0"
gem 'rack-mini-profiler', require: false 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' gem 'rerun', '~> 0.10.0'
# Better errors handler # Better errors handler
gem 'better_errors' gem 'better_errors', '~> 1.0.1'
gem 'binding_of_caller' gem 'binding_of_caller', '~> 0.7.2'
# Docs generator # Docs generator
gem "sdoc" gem "sdoc", '~> 0.3.20'
# thin instead webrick # thin instead webrick
gem 'thin' gem 'thin', '~> 1.6.1'
end end
group :development, :test do group :development, :test do
gem 'awesome_print'
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'fuubar', '~> 2.0.0'
gem 'pry-rails' 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 'database_cleaner', '~> 1.4.0'
gem 'factory_girl_rails' gem 'factory_girl_rails', '~> 4.3.0'
gem 'rspec-rails', '~> 3.3.0' gem 'rspec-rails', '~> 3.3.0'
gem 'rubocop', '0.28.0', require: false gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rails'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.3.0' gem 'minitest', '~> 5.7.0'
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.0.0' gem 'ffaker', '~> 2.0.0'
...@@ -248,20 +258,23 @@ group :development, :test do ...@@ -248,20 +258,23 @@ group :development, :test do
gem 'poltergeist', '~> 1.6.0' gem 'poltergeist', '~> 1.6.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine' gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.3.1' gem 'spring', '~> 1.3.6'
gem 'spring-commands-rspec', '~> 1.0.0' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' 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 end
group :test do group :test do
gem 'simplecov', require: false
gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0' gem 'webmock', '~> 1.21.0'
gem 'test_after_commit' gem 'test_after_commit', '~> 0.2.2'
gem 'sham_rack' gem 'sham_rack'
end end
...@@ -269,10 +282,32 @@ group :production do ...@@ -269,10 +282,32 @@ group :production do
gem "gitlab_meta", '7.0' gem "gitlab_meta", '7.0'
end 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.2" gem "mail_room", "~> 0.4.2"
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'
# 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
This diff is collapsed.
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} 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 # mail_room: bundle exec mail_room -q -c config/mail_room.yml
This diff is collapsed.
# 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 pager
#= 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 fa-refresh fa-spin"></i> Assign in progress..')
window.unbindEvents = ->
$(document).unbind('scroll')
$(document).off('scroll')
document.addEventListener("page:fetch", unbindEvents)
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 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
@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) =>
CiPager.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: ->
CiPager.disable
callback: (i) =>
unless $(".loading").is(':visible')
$(".loading").show()
CiPager.getItems()
$(document).on 'click', '.badge-codes-toggle', ->
$('.badge-codes-block').toggleClass("hide")
return false
$(document).on 'click', '.sync-now', ->
$(this).find('i').addClass('fa-spin')
...@@ -61,3 +61,9 @@ ...@@ -61,3 +61,9 @@
* Styles for JS behaviors. * Styles for JS behaviors.
*/ */
@import "behaviors.scss"; @import "behaviors.scss";
/**
* CI specific styles:
*/
@import "ci/**/*";
.ci-body {
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: 250px;
z-index: 100;
a {
display: block;
margin-bottom: 5px;
}
}
.page-sidebar-collapsed {
.scroll-controls {
left: 70px;
}
}
.build-widget {
padding: 10px;
background: $background-color;
margin-bottom: 20px;
border-radius: 4px;
.title {
margin-top: 0;
color: #666;
line-height: 1.5;
}
.attr-name {
color: #777;
}
}
.alert-disabled {
background: $background-color;
a {
color: #3084bb !important;
}
}
}
.ci-body {
.incorrect-syntax{
font-size: 19px;
color: red;
}
.correct-syntax{
font-size: 19px;
color: #47a447;
}
}
.ci-body {
.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;
}
}
.loading{
font-size: 20px;
}
.ci-charts {
fieldset {
margin-bottom: 16px;
}
}
}
.ci-body {
.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;
}
}
}
This diff is collapsed.
...@@ -134,9 +134,6 @@ class ApplicationController < ActionController::Base ...@@ -134,9 +134,6 @@ class ApplicationController < ActionController::Base
def repository def repository
@repository ||= project.repository @repository ||= project.repository
rescue Grit::NoSuchPathError => e
log_exception(e)
nil
end end
def authorize_project!(action) def authorize_project!(action)
......
module Ci
module Admin
class ApplicationController < Ci::ApplicationController
before_action :authenticate_user!
before_action :authenticate_admin!
layout "ci/admin"
end
end
end
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
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)
@builds =
case @scope
when "pending"
@builds.pending
when "running"
@builds.running
else
@builds
end
end
end
end
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
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
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
module Ci
class Admin::RunnersController < Ci::Admin::ApplicationController
before_action :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("ci_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
module Ci
class ApplicationController < ::ApplicationController
def self.railtie_helpers_paths
"app/helpers/ci"
end
helper_method :gl_project
private
def authenticate_public_page!
unless project.public
unless current_user
redirect_to(new_user_sessions_path) and return
end
return access_denied! unless can?(current_user, :read_project, gl_project)
end
end
def authenticate_token!
unless project.valid_token?(params[:token])
return head(403)
end
end
def authorize_access_project!
unless can?(current_user, :read_project, gl_project)
return page_404
end
end
def authorize_manage_builds!
unless can?(current_user, :admin_project, gl_project)
return page_404
end
end
def authenticate_admin!
return render_404 unless current_user.is_admin?
end
def authorize_manage_project!
unless can?(current_user, :admin_project, gl_project)
return page_404
end
end
def page_404
render file: "#{Rails.root}/public/404.html", status: 404, layout: false
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 gl_project
::Project.find(@project.gitlab_id)
end
end
end
module Ci
class BuildsController < Ci::ApplicationController
before_action :authenticate_user!, except: [:status, :show]
before_action :authenticate_public_page!, only: :show
before_action :project
before_action :authorize_access_project!, except: [:status, :show]
before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel]
before_action :authorize_manage_builds!, only: [:retry, :cancel]
before_action :build, except: [:show]
layout 'ci/build'
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
module Ci
class ChartsController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_access_project!
before_action :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
module Ci
class CommitsController < Ci::ApplicationController
before_action :authenticate_user!, except: [:status, :show]
before_action :authenticate_public_page!, only: :show
before_action :project
before_action :authorize_access_project!, except: [:status, :show, :cancel]
before_action :authorize_manage_builds!, only: [:cancel]
before_action :commit, only: :show
layout 'ci/commit'
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_commits_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
module Ci
class EventsController < Ci::ApplicationController
EVENTS_PER_PAGE = 50
before_action :authenticate_user!
before_action :project
before_action :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
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
module Ci
class LintsController < Ci::ApplicationController
before_action :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
module Ci
class ProjectsController < Ci::ApplicationController
PROJECTS_BATCH = 100
before_action :authenticate_user!, except: [:build, :badge, :index, :show]
before_action :authenticate_public_page!, only: :show
before_action :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
before_action :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create]
before_action :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
before_action :authenticate_token!, only: [:build]
before_action :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)
@gl_projects = current_user.authorized_projects
@gl_projects = @gl_projects.where("name LIKE ?", "%#{params[:search]}%") if params[:search]
@gl_projects = @gl_projects.page(@page).per(@limit)
@projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date
@total_count = @gl_projects.size
@gl_projects = @gl_projects.where.not(id: @projects.map(&:gitlab_id))
respond_to do |format|
format.json do
pager_json("ci/projects/gitlab", @total_count)
end
end
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 can?(current_user, :admin_project, ::Project.find(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.gl_project.gitlab_ci_service.update_attributes(active: false)
project.destroy
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
module Ci
class RunnerProjectsController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_manage_project!
layout 'ci/project'
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
return head(403) unless current_user.ci_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
module Ci
class RunnersController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
before_action :authorize_access_project!
before_action :authorize_manage_project!
layout 'ci/project'
def index
@runners = @project.runners.order('id DESC')
@specific_runners =
Ci::Runner.specific.includes(:runner_projects).
where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
where.not(id: @runners).order("#{Ci::Runner.table_name}.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
module Ci
class ServicesController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_access_project!
before_action :authorize_manage_project!
before_action :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)
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
module Ci
class TriggersController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_access_project!
before_action :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
module Ci
class VariablesController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_access_project!
before_action :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
module Ci
class WebHooksController < Ci::ApplicationController
before_action :authenticate_user!
before_action :project
before_action :authorize_access_project!
before_action :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
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include PageLayoutHelper include PageLayoutHelper
before_action :verify_user_oauth_applications_enabled before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user! before_action :authenticate_user!
......
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
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
module Ci
module CommitsHelper
def commit_status_alert_class(commit)
return 'alert-info' unless commit
case commit.status
when 'success'
'alert-success'
when 'failed', 'canceled'
'alert-danger'
when 'skipped'
'alert-disabled'
else
'alert-warning'
end
end
def ci_commit_path(commit)
ci_project_ref_commits_path(commit.project, commit.ref, commit.sha)
end
def commit_link(commit)
link_to(commit.short_sha, ci_commit_path(commit))
end
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
def ci_commit_title(commit)
content_tag :span do
link_to(
simple_sanitize(commit.project.name), ci_project_path(commit.project)
) + ' @ ' +
gitlab_commit_link(@project, @commit.sha)
end
end
end
end
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
module Ci
module IconsHelper
def boolean_to_icon(value)
if value.to_s == "true"
content_tag :i, nil, class: 'fa fa-circle cgreen'
else
content_tag :i, nil, class: 'fa fa-power-off clgray'
end
end
end
end
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
module Ci
module RoutesHelper
class Base
include Gitlab::Application.routes.url_helpers
def default_url_options
{
host: Settings.gitlab['host'],
protocol: Settings.gitlab['https'] ? "https" : "http",
port: Settings.gitlab['port']
}
end
end
def url_helpers
@url_helpers ||= Base.new
end
def self.method_missing(method, *args, &block)
@url_helpers ||= Base.new
if @url_helpers.respond_to?(method)
@url_helpers.send(method, *args, &block)
else
super method, *args, &block
end
end
end
end
module Ci
module RunnersHelper
def runner_status_icon(runner)
unless runner.contacted_at
return content_tag :i, nil,
class: "fa 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 fa-circle runner-status-#{status}",
title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
end
end
end
module Ci
module TriggersHelper
def ci_build_trigger_url(project_id, ref_name)
"#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
end
end
end
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
module Ci
module Emails
module Builds
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
end
end
end
module Ci
class Notify < ActionMailer::Base
include Ci::Emails::Builds
add_template_helper Ci::ApplicationHelper
add_template_helper Ci::GitlabHelper
default_url_options[:host] = Gitlab.config.gitlab.host
default_url_options[:protocol] = Gitlab.config.gitlab.protocol
default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
default from: Gitlab.config.gitlab.email_from
# Just send email with 3 seconds delay
def self.delay
delay_for(2.seconds)
end
private
# Formats arguments into a String suitable for use as an email subject
#
# extra - Extra Strings to be inserted into the subject
#
# Examples
#
# >> subject('Lorem ipsum')
# => "GitLab-CI | Lorem ipsum"
#
# # Automatically inserts Project name when @project is set
# >> @project = Project.last
# => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
# >> subject('Lorem ipsum')
# => "GitLab-CI | Ruby on Rails | Lorem ipsum "
#
# # Accepts multiple arguments
# >> subject('Lorem ipsum', 'Dolor sit amet')
# => "GitLab-CI | Lorem ipsum | Dolor sit amet"
def subject(*extra)
subject = "GitLab-CI"
subject << (@project ? " | #{@project.name}" : "")
subject << " | " + extra.join(' | ') if extra.present?
subject
end
end
end
...@@ -100,7 +100,7 @@ class Notify < BaseMailer ...@@ -100,7 +100,7 @@ class Notify < BaseMailer
def mail_thread(model, headers = {}) def mail_thread(model, headers = {})
if @project if @project
headers['X-GitLab-Project'] = @project.name headers['X-GitLab-Project'] = @project.name
headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end end
......
...@@ -149,6 +149,7 @@ class Ability ...@@ -149,6 +149,7 @@ class Ability
:admin_merge_request, :admin_merge_request,
:create_merge_request, :create_merge_request,
:create_wiki, :create_wiki,
:manage_builds,
:push_code :push_code
] ]
end end
......
# == Schema Information
#
# Table name: application_settings
#
# id :integer not null, primary key
# all_broken_builds :boolean
# add_pusher :boolean
# created_at :datetime
# updated_at :datetime
#
module Ci
class ApplicationSetting < ActiveRecord::Base
extend Ci::Model
def self.current
Ci::ApplicationSetting.last
end
def self.create_from_defaults
create(
all_broken_builds: Settings.gitlab_ci['all_broken_builds'],
add_pusher: Settings.gitlab_ci['add_pusher'],
)
end
end
end
# == Schema Information
#
# Table name: builds
#
# id :integer not null, primary key
# project_id :integer
# status :string(255)
# finished_at :datetime
# trace :text
# created_at :datetime
# updated_at :datetime
# started_at :datetime
# runner_id :integer
# commit_id :integer
# coverage :float
# commands :text
# job_id :integer
# name :string(255)
# options :text
# allow_failure :boolean default(FALSE), not null
# stage :string(255)
# deploy :boolean default(FALSE)
# trigger_request_id :integer
#
module Ci
class Build < ActiveRecord::Base
extend Ci::Model
LAZY_ATTRIBUTES = ['trace']
belongs_to :commit, class_name: 'Ci::Commit'
belongs_to :project, class_name: 'Ci::Project'
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
serialize :options
validates :commit, presence: true
validates :status, presence: true
validates :coverage, numericality: true, allow_blank: true
scope :running, ->() { where(status: "running") }
scope :pending, ->() { where(status: "pending") }
scope :success, ->() { where(status: "success") }
scope :failed, ->() { where(status: "failed") }
scope :unstarted, ->() { where(runner_id: nil) }
scope :running_or_pending, ->() { where(status:[:running, :pending]) }
acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
class << self
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
"#{table_name}.#{column_name}"
end
end
def last_month
where('created_at > ?', Date.today - 1.month)
end
def first_pending
pending.unstarted.order('created_at ASC').first
end
def create_from(build)
new_build = build.dup
new_build.status = :pending
new_build.runner_id = nil
new_build.save
end
def retry(build)
new_build = Ci::Build.new(status: :pending)
new_build.options = build.options
new_build.commands = build.commands
new_build.tag_list = build.tag_list
new_build.commit_id = build.commit_id
new_build.project_id = build.project_id
new_build.name = build.name
new_build.allow_failure = build.allow_failure
new_build.stage = build.stage
new_build.trigger_request = build.trigger_request
new_build.save
new_build
end
end
state_machine :status, initial: :pending do
event :run do
transition pending: :running
end
event :drop do
transition running: :failed
end
event :success do
transition running: :success
end
event :cancel do
transition [:pending, :running] => :canceled
end
after_transition pending: :running do |build, transition|
build.update_attributes started_at: Time.now
end
after_transition any => [:success, :failed, :canceled] do |build, transition|
build.update_attributes finished_at: Time.now
project = build.project
if project.web_hooks?
Ci::WebHookService.new.build_end(build)
end
if build.commit.success?
build.commit.create_next_builds(build.trigger_request)
end
project.execute_services(build)
if project.coverage_enabled?
build.update_coverage
end
end
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
state :success, value: 'success'
state :canceled, value: 'canceled'
end
delegate :sha, :short_sha, :before_sha, :ref,
to: :commit, prefix: false
def trace_html
html = Ci::Ansi2html::convert(trace) if trace.present?
html ||= ''
end
def trace
if project && read_attribute(:trace).present?
read_attribute(:trace).gsub(project.token, 'xxxxxx')
end
end
def started?
!pending? && !canceled? && started_at
end
def active?
running? || pending?
end
def complete?
canceled? || success? || failed?
end
def ignored?
failed? && allow_failure?
end
def timeout
project.timeout
end
def variables
yaml_variables + project_variables + trigger_variables
end
def duration
if started_at && finished_at
finished_at - started_at
elsif started_at
Time.now - started_at
end
end
def project
commit.project
end
def project_id
commit.project_id
end
def project_name
project.name
end
def repo_url
project.repo_url_with_auth
end
def allow_git_fetch
project.allow_git_fetch
end
def update_coverage
coverage = extract_coverage(trace, project.coverage_regex)
if coverage.is_a? Numeric
update_attributes(coverage: coverage)
end
end
def extract_coverage(text, regex)
begin
matches = text.gsub(Regexp.new(regex)).to_a.last
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
coverage.to_f
end
rescue => ex
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
end
end
def trace
if File.exist?(path_to_trace)
File.read(path_to_trace)
else
# backward compatibility
read_attribute :trace
end
end
def trace=(trace)
unless Dir.exists? dir_to_trace
FileUtils.mkdir_p dir_to_trace
end
File.write(path_to_trace, trace)
end
def dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
created_at.utc.strftime("%Y_%m"),
project.id.to_s
)
end
def path_to_trace
"#{dir_to_trace}/#{id}.log"
end
private
def yaml_variables
if commit.config_processor
commit.config_processor.variables.map do |key, value|
{ key: key, value: value, public: true }
end
else
[]
end
end
def project_variables
project.variables.map do |variable|
{ key: variable.key, value: variable.value, public: false }
end
end
def trigger_variables
if trigger_request && trigger_request.variables
trigger_request.variables.map do |key, value|
{ key: key, value: value, public: false }
end
else
[]
end
end
end
end
# == Schema Information
#
# Table name: commits
#
# id :integer not null, primary key
# project_id :integer
# ref :string(255)
# sha :string(255)
# before_sha :string(255)
# push_data :text
# created_at :datetime
# updated_at :datetime
# tag :boolean default(FALSE)
# yaml_errors :text
# committed_at :datetime
#
module Ci
class Commit < ActiveRecord::Base
extend Ci::Model
belongs_to :project, class_name: 'Ci::Project'
has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
serialize :push_data
validates_presence_of :ref, :sha, :before_sha, :push_data
validate :valid_commit_sha
def self.truncate_sha(sha)
sha[0...8]
end
def to_param
sha
end
def last_build
builds.order(:id).last
end
def retry
builds_without_retry.each do |build|
Ci::Build.retry(build)
end
end
def valid_commit_sha
if self.sha == Ci::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
end
end
def new_branch?
before_sha == Ci::Git::BLANK_SHA
end
def compare?
!new_branch?
end
def git_author_name
commit_data[:author][:name] if commit_data && commit_data[:author]
end
def git_author_email
commit_data[:author][:email] if commit_data && commit_data[:author]
end
def git_commit_message
commit_data[:message] if commit_data && commit_data[:message]
end
def short_before_sha
Ci::Commit.truncate_sha(before_sha)
end
def short_sha
Ci::Commit.truncate_sha(sha)
end
def commit_data
push_data[:commits].find do |commit|
commit[:id] == sha
end
rescue
nil
end
def project_recipients
recipients = project.email_recipients.split(' ')
if project.email_add_pusher? && push_data[:user_email].present?
recipients << push_data[:user_email]
end
recipients.uniq
end
def stage
return unless config_processor
stages = builds_without_retry.select(&:active?).map(&:stage)
config_processor.stages.find { |stage| stages.include? stage }
end
def create_builds_for_stage(stage, trigger_request)
return if skip_ci? && trigger_request.blank?
return unless config_processor
builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag)
builds_attrs.map do |build_attrs|
builds.create!({
project: project,
name: build_attrs[:name],
commands: build_attrs[:script],
tag_list: build_attrs[:tags],
options: build_attrs[:options],
allow_failure: build_attrs[:allow_failure],
stage: build_attrs[:stage],
trigger_request: trigger_request,
})
end
end
def create_next_builds(trigger_request)
return if skip_ci? && trigger_request.blank?
return unless config_processor
stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
config_processor.stages.any? do |stage|
!stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
end
end
def create_builds(trigger_request = nil)
return if skip_ci? && trigger_request.blank?
return unless config_processor
config_processor.stages.any? do |stage|
create_builds_for_stage(stage, trigger_request).present?
end
end
def builds_without_retry
@builds_without_retry ||=
begin
grouped_builds = builds.group_by(&:name)
grouped_builds.map do |name, builds|
builds.sort_by(&:id).last
end
end
end
def builds_without_retry_sorted
return builds_without_retry unless config_processor
stages = config_processor.stages
builds_without_retry.sort_by do |build|
[stages.index(build.stage) || -1, build.name || ""]
end
end
def retried_builds
@retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
end
def status
if skip_ci?
return 'skipped'
elsif yaml_errors.present?
return 'failed'
elsif builds.none?
return 'skipped'
elsif success?
'success'
elsif pending?
'pending'
elsif running?
'running'
elsif canceled?
'canceled'
else
'failed'
end
end
def pending?
builds_without_retry.all? do |build|
build.pending?
end
end
def running?
builds_without_retry.any? do |build|
build.running? || build.pending?
end
end
def success?
builds_without_retry.all? do |build|
build.success? || build.ignored?
end
end
def failed?
status == 'failed'
end
def canceled?
builds_without_retry.all? do |build|
build.canceled?
end
end
def duration
@duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
end
def finished_at
@finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
end
def coverage
if project.coverage_enabled?
coverage_array = builds_without_retry.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
end
def matrix?
builds_without_retry.size > 1
end
def config_processor
@config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config)
rescue Ci::GitlabCiYamlProcessor::ValidationError => e
save_yaml_error(e.message)
nil
rescue Exception => e
logger.error e.message + "\n" + e.backtrace.join("\n")
save_yaml_error("Undefined yaml error")
nil
end
def skip_ci?
return false if builds.any?
commits = push_data[:commits]
commits.present? && commits.last[:message] =~ /(\[ci skip\])/
end
def update_committed!
update!(committed_at: DateTime.now)
end
private
def save_yaml_error(error)
return if self.yaml_errors?
self.yaml_errors = error
save
end
end
end
# == Schema Information
#
# Table name: events
#
# id :integer not null, primary key
# project_id :integer
# user_id :integer
# is_admin :integer
# description :text
# created_at :datetime
# updated_at :datetime
#
module Ci
class Event < ActiveRecord::Base
extend Ci::Model
belongs_to :project, class_name: 'Ci::Project'
validates :description,
presence: true,
length: { in: 5..200 }
scope :admin, ->(){ where(is_admin: true) }
scope :project_wide, ->(){ where(is_admin: false) }
end
end
This diff is collapsed.
module Ci
module ProjectStatus
def status
last_commit.status if last_commit
end
def broken?
last_commit.failed? if last_commit
end
def success?
last_commit.success? if last_commit
end
def broken_or_success?
broken? || success?
end
def last_commit
@last_commit ||= commits.last if commits.any?
end
def last_commit_date
last_commit.try(:created_at)
end
def human_status
status
end
# only check for toggling build status within same ref.
def last_commit_changed_status?
ref = last_commit.ref
last_commits = commits.where(ref: ref).last(2)
if last_commits.size < 2
false
else
last_commits[0].status != last_commits[1].status
end
end
def last_commit_for_ref(ref)
commits.where(ref: ref).last
end
end
end
This diff is collapsed.
# == Schema Information
#
# Table name: runner_projects
#
# id :integer not null, primary key
# runner_id :integer not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :project, class_name: 'Ci::Project'
validates_uniqueness_of :runner_id, scope: :project_id
end
end
This diff is collapsed.
# == Schema Information
#
# Table name: triggers
#
# id :integer not null, primary key
# token :string(255)
# project_id :integer not null
# deleted_at :datetime
# created_at :datetime
# updated_at :datetime
#
module Ci
class Trigger < ActiveRecord::Base
extend Ci::Model
acts_as_paranoid
belongs_to :project, class_name: 'Ci::Project'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
validates_presence_of :token
validates_uniqueness_of :token
before_validation :set_default_values
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
def last_trigger_request
trigger_requests.last
end
def short_token
token[0...10]
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
# #
class GitlabIssueTrackerService < IssueTrackerService class GitlabIssueTrackerService < IssueTrackerService
include Rails.application.routes.url_helpers include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment