Commit d53b4d5b authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'master' into 8-8-stable

parents a08e3c67 f26389a0
...@@ -21,7 +21,7 @@ AllCops: ...@@ -21,7 +21,7 @@ AllCops:
- 'lib/email_validator.rb' - 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb' - 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb' - 'lib/gitlab/seeder.rb'
- 'lib/templates/**/*' - 'generator_templates/**/*'
##################### Style ################################## ##################### Style ##################################
......
...@@ -6,8 +6,10 @@ v 8.8.0 (unreleased) ...@@ -6,8 +6,10 @@ v 8.8.0 (unreleased)
- Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen)
- Use a case-insensitive comparison in sanitizing URI schemes - Use a case-insensitive comparison in sanitizing URI schemes
- Toggle sign-up confirmation emails in application settings - Toggle sign-up confirmation emails in application settings
- Make it possible to prevent tagged runner from picking untagged jobs
- Project#open_branches has been cleaned up and no longer loads entire records into memory. - Project#open_branches has been cleaned up and no longer loads entire records into memory.
- Escape HTML in commit titles in system note messages - Escape HTML in commit titles in system note messages
- Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios
- Improve multiple branch push performance by memoizing permission checking - Improve multiple branch push performance by memoizing permission checking
- Log to application.log when an admin starts and stops impersonating a user - Log to application.log when an admin starts and stops impersonating a user
- Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi) - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi)
......
@import "framework/variables";
table.code {
width: 100%;
font-family: monospace;
border: none;
border-collapse: separate;
margin: 0;
padding: 0;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
td {
line-height: $code_line_height;
font-family: monospace;
font-size: $code_font_size;
}
td.diff-line-num {
margin: 0;
padding: 0;
border: none;
background: $background-color;
color: rgba(0, 0, 0, 0.3);
padding: 0 5px;
border-right: 1px solid $border-color;
text-align: right;
min-width: 35px;
max-width: 50px;
width: 35px;
}
td.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
}
}
@import "highlight/white";
.pipeline-stage {
overflow: hidden;
text-overflow: ellipsis;
}
...@@ -9,24 +9,19 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -9,24 +9,19 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def show def show
@builds = @runner.builds.order('id DESC').first(30) assign_builds_and_projects
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end end
def update def update
@runner.update_attributes(runner_params) if @runner.update_attributes(runner_params)
respond_to do |format| respond_to do |format|
format.js format.js
format.html { redirect_to admin_runner_path(@runner) } format.html { redirect_to admin_runner_path(@runner) }
end end
else
assign_builds_and_projects
render 'show'
end
end end
def destroy def destroy
...@@ -60,4 +55,16 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -60,4 +55,16 @@ class Admin::RunnersController < Admin::ApplicationController
def runner_params def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end end
def assign_builds_and_projects
@builds = runner.builds.order('id DESC').first(30)
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end
end end
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
before_action :commit, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
def index
@scope = params[:scope]
all_pipelines = project.ci_commits
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
end
def new
@pipeline = project.ci_commits.new(ref: @project.default_branch)
end
def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
unless @pipeline.persisted?
render 'new'
return
end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end
def show
end
def retry
pipeline.retry_failed
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
def cancel
pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
private
def create_params
params.require(:pipeline).permit(:ref)
end
def pipeline
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
end
def commit
@commit ||= @pipeline.commit_data
end
end
...@@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController
if @runner.update_attributes(runner_params) if @runner.update_attributes(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
redirect_to runner_path(@runner), alert: 'Runner was not updated.' render 'edit'
end end
end end
......
class PipelinesFinder
attr_reader :project
def initialize(project)
@project = project
end
def execute(pipelines, scope)
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(pipelines, ids_for_ref(pipelines, branches))
when 'tags'
from_ids(pipelines, ids_for_ref(pipelines, tags))
else
pipelines
end
end
private
def ids_for_ref(pipelines, refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(pipelines, ids)
pipelines.unscoped.where(id: ids)
end
def branches
project.repository.branches.map(&:name)
end
def tags
project.repository.tags.map(&:name)
end
end
...@@ -36,7 +36,7 @@ class TodosFinder ...@@ -36,7 +36,7 @@ class TodosFinder
private private
def action_id? def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i) action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
end end
def action_id def action_id
......
...@@ -38,19 +38,30 @@ module CiStatusHelper ...@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_ci_status(ci_commit, tooltip_placement: 'auto left') def render_commit_status(commit, tooltip_placement: 'auto left')
# TODO: split this method into project = commit.project
# - render_commit_status path = builds_namespace_project_commit_path(project.namespace, project, commit)
# - render_pipeline_status render_status_with_link('commit', commit.status, path, tooltip_placement)
link_to ci_icon_for_status(ci_commit.status), end
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
title: "Build #{ci_label_for_status(ci_commit.status)}", project = pipeline.project
data: { toggle: 'tooltip', placement: tooltip_placement } path = namespace_project_pipeline_path(project.namespace, project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end end
def no_runners_for_project?(project) def no_runners_for_project?(project)
project.runners.blank? && project.runners.blank? &&
Ci::Runner.shared.blank? Ci::Runner.shared.blank?
end end
private
def render_status_with_link(type, status, path, tooltip_placement)
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
end end
...@@ -32,12 +32,6 @@ module EmailsHelper ...@@ -32,12 +32,6 @@ module EmailsHelper
nil nil
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 def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60 valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24 if valid_hours >= 24
......
...@@ -11,6 +11,7 @@ module TodosHelper ...@@ -11,6 +11,7 @@ module TodosHelper
case todo.action case todo.action
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
end end
end end
...@@ -28,8 +29,11 @@ module TodosHelper ...@@ -28,8 +29,11 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor) todo.target, anchor: anchor)
else else
polymorphic_path([todo.project.namespace.becomes(Namespace), path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
todo.project, todo.target], anchor: anchor)
path.unshift(:builds) if todo.build_failed?
polymorphic_path(path, anchor: anchor)
end end
end end
......
...@@ -65,7 +65,8 @@ module Emails ...@@ -65,7 +65,8 @@ module Emails
# used in notify layout # used in notify layout
@target_url = @message.target_url @target_url = @message.target_url
@project = Project.find project_id @project = Project.find(project_id)
@diff_notes_disabled = true
add_project_headers add_project_headers
headers['X-GitLab-Author'] = @message.author_username headers['X-GitLab-Author'] = @message.author_username
......
...@@ -10,6 +10,8 @@ class Notify < BaseMailer ...@@ -10,6 +10,8 @@ class Notify < BaseMailer
include Emails::Builds include Emails::Builds
add_template_helper MergeRequestsHelper add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper add_template_helper EmailsHelper
def test_email(recipient_email, subject, body) def test_email(recipient_email, subject, body)
......
...@@ -205,6 +205,7 @@ class Ability ...@@ -205,6 +205,7 @@ class Ability
:read_commit_status, :read_commit_status,
:read_build, :read_build,
:read_container_image, :read_container_image,
:read_pipeline,
] ]
end end
...@@ -216,6 +217,8 @@ class Ability ...@@ -216,6 +217,8 @@ class Ability
:update_commit_status, :update_commit_status,
:create_build, :create_build,
:update_build, :update_build,
:create_pipeline,
:update_pipeline,
:create_merge_request, :create_merge_request,
:create_wiki, :create_wiki,
:push_code, :push_code,
...@@ -248,6 +251,7 @@ class Ability ...@@ -248,6 +251,7 @@ class Ability
:admin_commit_status, :admin_commit_status,
:admin_build, :admin_build,
:admin_container_image, :admin_container_image,
:admin_pipeline
] ]
end end
...@@ -290,6 +294,7 @@ class Ability ...@@ -290,6 +294,7 @@ class Ability
unless project.builds_enabled unless project.builds_enabled
rules += named_abilities('build') rules += named_abilities('build')
rules += named_abilities('pipeline')
end end
unless project.container_registry_enabled unless project.container_registry_enabled
......
...@@ -53,6 +53,7 @@ module Ci ...@@ -53,6 +53,7 @@ module Ci
new_build.stage_idx = build.stage_idx new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request new_build.trigger_request = build.trigger_request
new_build.save new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build new_build
end end
end end
...@@ -290,9 +291,15 @@ module Ci ...@@ -290,9 +291,15 @@ module Ci
end end
def can_be_served?(runner) def can_be_served?(runner)
return false unless has_tags? || runner.run_untagged?
(tag_list - runner.tag_list).empty? (tag_list - runner.tag_list).empty?
end end
def has_tags?
tag_list.any?
end
def any_runners_online? def any_runners_online?
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end end
......
...@@ -8,8 +8,6 @@ module Ci ...@@ -8,8 +8,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
delegate :stages, to: :statuses
validates_presence_of :sha validates_presence_of :sha
validates_presence_of :status validates_presence_of :status
validate :valid_commit_sha validate :valid_commit_sha
...@@ -22,7 +20,8 @@ module Ci ...@@ -22,7 +20,8 @@ module Ci
end end
def self.stages def self.stages
CommitStatus.where(commit: all).stages # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(commit: pluck(:id)).stages
end end
def project_id def project_id
...@@ -67,6 +66,25 @@ module Ci ...@@ -67,6 +66,25 @@ module Ci
end end
end end
def cancel_running
builds.running_or_pending.each(&:cancel)
end
def retry_failed
builds.latest.failed.select(&:retryable?).each(&:retry)
end
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
def triggered?
trigger_requests.any?
end
def create_builds(user, trigger_request = nil) def create_builds(user, trigger_request = nil)
return unless config_processor return unless config_processor
config_processor.stages.any? do |stage| config_processor.stages.any? do |stage|
......
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active] FORM_EDITABLE = %i[description tag_list active run_untagged]
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
...@@ -26,6 +26,8 @@ module Ci ...@@ -26,6 +26,8 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end end
validate :tag_constraints
acts_as_taggable acts_as_taggable
# Searches for runners matching the given query. # Searches for runners matching the given query.
...@@ -96,5 +98,18 @@ module Ci ...@@ -96,5 +98,18 @@ module Ci
def short_sha def short_sha
token[0...8] if token token[0...8] if token
end end
def has_tags?
tag_list.any?
end
private
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
end end
end end
...@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
...@@ -45,6 +46,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -45,6 +46,10 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status| after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
end
end end
delegate :sha, :short_sha, to: :commit delegate :sha, :short_sha, to: :commit
...@@ -54,13 +59,15 @@ class CommitStatus < ActiveRecord::Base ...@@ -54,13 +59,15 @@ class CommitStatus < ActiveRecord::Base
end end
def self.stages def self.stages
order_by = 'max(stage_idx)' # We group by stage name, but order stages by theirs' index
group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end end
def self.stages_status def self.stages_status
all.stages.inject({}) do |h, stage| # We execute subquery for each stage to calculate a stage status
h[stage] = all.where(stage: stage).status statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
statuses.inject({}) do |h, k|
h[k.first] = k.last
h h
end end
end end
......
class Todo < ActiveRecord::Base class Todo < ActiveRecord::Base
ASSIGNED = 1 ASSIGNED = 1
MENTIONED = 2 MENTIONED = 2
BUILD_FAILED = 3
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :note belongs_to :note
...@@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base ...@@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base
state :done state :done
end end
def build_failed?
action == BUILD_FAILED
end
def body def body
if note.present? if note.present?
note.note note.note
......
module Ci
class CreatePipelineService < BaseService
def execute
pipeline = project.ci_commits.new(params)
unless ref_names.include?(params[:ref])
pipeline.errors.add(:base, 'Reference not found')
return pipeline
end
unless commit
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
unless can?(current_user, :create_pipeline, project)
pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
return pipeline
end
begin
Ci::Commit.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback
end
pipeline.save!
pipeline.create_builds(current_user)
end
rescue
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
end
pipeline
end
private
def ref_names
@ref_names ||= project.repository.ref_names
end
def commit
@commit ||= project.commit(params[:ref])
end
end
end
...@@ -18,9 +18,7 @@ class CreateCommitBuildsService ...@@ -18,9 +18,7 @@ class CreateCommitBuildsService
return false return false
end end
commit = project.ci_commit(sha, ref) commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
unless commit
commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found # Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file unless commit.ci_yaml_file
...@@ -29,7 +27,6 @@ class CreateCommitBuildsService ...@@ -29,7 +27,6 @@ class CreateCommitBuildsService
# Create a new ci_commit # Create a new ci_commit
commit.save! commit.save!
end
# Skip creating builds for commits that have [ci skip] # Skip creating builds for commits that have [ci skip]
unless commit.skip_ci? unless commit.skip_ci?
......
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
end
end
...@@ -38,5 +38,30 @@ module MergeRequests ...@@ -38,5 +38,30 @@ module MergeRequests
def filter_params def filter_params
super(:merge_request) super(:merge_request)
end end
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
def each_merge_request(commit_status)
merge_request_from(commit_status).each do |merge_request|
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
yield merge_request, ci_commit
end
end
end end
end end
...@@ -20,15 +20,9 @@ module MergeRequests ...@@ -20,15 +20,9 @@ module MergeRequests
# Triggers the automatic merge of merge_request once the build succeeds # Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status) def trigger(commit_status)
merge_requests = merge_request_from(commit_status) each_merge_request(commit_status) do |merge_request, ci_commit|
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds? next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable? next unless merge_request.mergeable?
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
next unless ci_commit.success? next unless ci_commit.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
...@@ -47,20 +41,5 @@ module MergeRequests ...@@ -47,20 +41,5 @@ module MergeRequests
end end
end end
private
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
end end
end end
...@@ -12,6 +12,7 @@ module MergeRequests ...@@ -12,6 +12,7 @@ module MergeRequests
close_merge_requests close_merge_requests
reload_merge_requests reload_merge_requests
reset_merge_when_build_succeeds reset_merge_when_build_succeeds
mark_pending_todos_done
# Leave a system note if a branch was deleted/added # Leave a system note if a branch was deleted/added
if branch_added? || branch_removed? if branch_added? || branch_removed?
...@@ -80,6 +81,12 @@ module MergeRequests ...@@ -80,6 +81,12 @@ module MergeRequests
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end end
def mark_pending_todos_done
merge_requests_for_source_branch.each do |merge_request|
todo_service.merge_request_push(merge_request, @current_user)
end
end
def find_new_commits def find_new_commits
if branch_added? if branch_added?
@commits = [] @commits = []
......
...@@ -80,6 +80,30 @@ class TodoService ...@@ -80,6 +80,30 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user) mark_pending_todos_as_done(merge_request, current_user)
end end
# When a build fails on the HEAD of a merge request we should:
#
# * create a todo for that user to fix it
#
def merge_request_build_failed(merge_request)
create_build_failed_todo(merge_request)
end
# When a new commit is pushed to a merge request we should:
#
# * mark all pending todos related to the merge request for that user as done
#
def merge_request_push(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
# When a build is retried to a merge request we should:
#
# * mark all pending todos related to the merge request for the author as done
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
end
# When create a note we should: # When create a note we should:
# #
# * mark all pending todos related to the noteable for the note author as done # * mark all pending todos related to the noteable for the note author as done
...@@ -145,6 +169,12 @@ class TodoService ...@@ -145,6 +169,12 @@ class TodoService
create_todos(mentioned_users, attributes) create_todos(mentioned_users, attributes)
end end
def create_build_failed_todo(merge_request)
author = merge_request.author
attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
create_todos(author, attributes)
end
def attributes_for_target(target) def attributes_for_target(target)
attributes = { attributes = {
project_id: target.project.id, project_id: target.project.id,
......
...@@ -9,8 +9,6 @@ ...@@ -9,8 +9,6 @@
%span.runner-state.runner-state-specific %span.runner-state.runner-state-specific
Specific Specific
- if @runner.shared? - if @runner.shared?
.bs-callout.bs-callout-success .bs-callout.bs-callout-success
%h4 This runner will process builds from ALL UNASSIGNED projects %h4 This runner will process builds from ALL UNASSIGNED projects
......
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title .todo-title.title
- unless todo.build_failed?
%span.author-name %span.author-name
- if todo.author - if todo.author
= link_to_author(todo) = link_to_author(todo)
......
...@@ -39,6 +39,13 @@ ...@@ -39,6 +39,13 @@
Commits Commits
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
= icon('ship fw')
%span
Pipelines
%span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count)
= nav_link(controller: %w(builds)) do = nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw') = icon('cubes fw')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
%title %title
GitLab GitLab
= stylesheet_link_tag 'notify' = stylesheet_link_tag 'notify'
= yield :head
%body %body
%div.content %div.content
= yield = yield
......
= content_for :head do
= stylesheet_link_tag 'mailers/repository_push_email'
%h3 %h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))}
...@@ -43,26 +46,38 @@ ...@@ -43,26 +46,38 @@
= diff.new_path = diff.new_path
- unless @message.disable_diffs? - unless @message.disable_diffs?
- diff_files = @message.diffs
- if @message.compare_timeout
%h5 The diff was not included because it is too large.
- else
%h4 Changes: %h4 Changes:
- @message.diffs.each_with_index do |diff, i| - diff_files.each_with_index do |diff_file, i|
%li{id: "diff-#{i}"} %li{id: "diff-#{i}"}
%a{href: @message.target_url + "#diff-#{i}"} %a{href: @message.target_url + "#diff-#{i}"}<
- if diff.deleted_file - if diff_file.deleted_file
%strong %strong<
= diff.old_path = diff_file.old_path
deleted deleted
- elsif diff.renamed_file - elsif diff_file.renamed_file
%strong %strong<
= diff.old_path = diff_file.old_path
&rarr; &rarr;
%strong %strong<
= diff.new_path = diff_file.new_path
- else
%strong<
= diff_file.new_path
- if diff_file.too_large?
The diff for this file was not included because it is too large.
- else - else
%strong
= diff.new_path
%hr %hr
= color_email_diff(diff.diff) - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last
- blob = @message.project.repository.blob_for_diff(diff_commit, diff_file)
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true}
- else
No preview for this file type
%br %br
- if @message.compare_timeout
%h5 Huge diff. To prevent performance issues changes are hidden
...@@ -25,24 +25,28 @@ ...@@ -25,24 +25,28 @@
- else - else
\- #{diff.new_path} \- #{diff.new_path}
- unless @message.disable_diffs? - unless @message.disable_diffs?
- if @message.compare_timeout
\
\
The diff was not included because it is too large.
- else
\ \
\ \
Changes: Changes:
- @message.diffs.each do |diff| - @message.diffs.each do |diff_file|
\ \
\===================================== \=====================================
- if diff.deleted_file - if diff_file.deleted_file
#{diff.old_path} deleted #{diff_file.old_path} deleted
- elsif diff.renamed_file - elsif diff_file.renamed_file
#{diff.old_path}#{diff.new_path} #{diff_file.old_path}#{diff_file.new_path}
- else - else
= diff.new_path = diff_file.new_path
\===================================== \=====================================
!= diff.diff - if diff_file.too_large?
- if @message.compare_timeout The diff for this file was not included because it is too large.
\ - else
\ != diff_file.diff.diff
Huge diff. To prevent performance issues it was hidden
- if @message.target_url - if @message.target_url
\ \
\ \
......
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
%strong ##{build.id} %strong ##{build.id}
- if build.stuck? - if build.stuck?
%i.fa.fa-warning.text-warning = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- if defined?(commit_sha) && commit_sha - if defined?(commit_sha) && commit_sha
%td %td
...@@ -40,7 +42,7 @@ ...@@ -40,7 +42,7 @@
%td %td
= build.name = build.name
%td .pull-right
.label-container .label-container
- if build.tags.any? - if build.tags.any?
- build.tags.each do |tag| - build.tags.each do |tag|
...@@ -55,10 +57,14 @@ ...@@ -55,10 +57,14 @@
%td.duration %td.duration
- if build.duration - if build.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(build.finished_at, build.started_at)} #{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp %td.timestamp
- if build.finished_at - if build.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(build.finished_at)} %span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage - if defined?(coverage) && coverage
...@@ -70,11 +76,11 @@ ...@@ -70,11 +76,11 @@
.pull-right .pull-right
- if can?(current_user, :read_build, build) && build.artifacts? - if can?(current_user, :read_build, build) && build.artifacts?
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
%i.fa.fa-download = icon('download')
- if can?(current_user, :update_build, build) - if can?(current_user, :update_build, build)
- if build.active? - if build.active?
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
%i.fa.fa-remove.cred = icon('remove', class: 'cred')
- elsif defined?(allow_retry) && allow_retry && build.retryable? - elsif defined?(allow_retry) && allow_retry && build.retryable?
= link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
%i.fa.fa-refresh = icon('refresh')
- status = commit.status
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
%strong ##{commit.id}
%td
%div.branch-commit
- if commit.ref
= link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace"
&middot;
= link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"
&nbsp;
- if commit.latest?
%span.label.label-success latest
- if commit.tag?
%span.label.label-primary tag
- if commit.triggered?
%span.label.label-primary triggered
- if commit.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid
- if commit.builds.any?(&:stuck?)
%span.label.label-warning stuck
%p
%span
- if commit_data = commit.commit_data
= link_to_gfm commit_data.title, namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
- stages_status = commit.statuses.stages_status
- stages.each do |stage|
%td
- if status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status}"
%span.has-tooltip{ title: "#{tooltip}", class: "ci-status-icon-#{status}" }
= ci_icon_for_status(status)
%td
- if commit.started_at && commit.finished_at
%p
= icon("clock-o")
&nbsp;
#{duration_in_words(commit.finished_at, commit.started_at)}
- if commit.finished_at
%p
= icon("calendar")
&nbsp;
#{time_ago_with_tooltip(commit.finished_at)}
%td
.controls.hidden-xs.pull-right
- artifacts = commit.builds.latest.select { |b| b.artifacts? }
- if artifacts.present?
.dropdown.inline.build-artifacts
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
= icon('download')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
= icon("download")
%span #{build.name}
- if can?(current_user, :update_pipeline, @project)
&nbsp;
- if commit.retryable? && commit.builds.failed.any?
= link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= icon("repeat")
&nbsp;
- if commit.active?
= link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= icon("remove")
- @ci_commits.each do |ci_commit| - @ci_commits.each do |ci_commit|
= render "ci_commit", ci_commit: ci_commit = render "ci_commit", ci_commit: ci_commit, pipeline_details: true
.row-content-block.build-content.middle-block .row-content-block.build-content.middle-block
.pull-right .pull-right
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_pipeline, @project)
- if ci_commit.builds.latest.failed.any?(&:retryable?) - if ci_commit.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post = link_to "Retry failed", retry_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post
- if ci_commit.builds.running_or_pending.any? - if ci_commit.builds.running_or_pending.any?
= link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post = link_to "Cancel running", cancel_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
.oneline .oneline.clearfix
- if defined?(pipeline_details) && pipeline_details
Pipeline
= link_to "##{ci_commit.id}", namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: "monospace"
with
= pluralize ci_commit.statuses.count(:id), "build" = pluralize ci_commit.statuses.count(:id), "build"
- if ci_commit.ref - if ci_commit.ref
for for
%span.label.label-info = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace"
= ci_commit.ref
- if defined?(link_to_commit) && link_to_commit - if defined?(link_to_commit) && link_to_commit
for commit for commit
= link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
...@@ -34,38 +37,5 @@ ...@@ -34,38 +37,5 @@
.table-holder .table-holder
%table.table.builds %table.table.builds
%thead - ci_commit.statuses.stages.each do |stage|
%tr = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage)
%th Status
%th Build ID
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
- builds = ci_commit.statuses.latest.ordered
= render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true
- if ci_commit.retried.any?
.row-content-block.second-block
Retried builds
.table-holder
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Ref
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
= render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false
%tr
%th{colspan: 10}
%strong
- status = statuses.latest.status
%span{class: "ci-status-link ci-status-icon-#{status}"}
= ci_icon_for_status(status)
- if stage
&nbsp;
= stage.titleize.pluralize
= render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
= render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
%td{colspan: 10}
&nbsp;
.pull-right.commit-action-buttons .pull-right.commit-action-buttons
%div %div
- if @notes_count > 0 - if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped %span.btn.disabled.btn-grouped
%i.fa.fa-comment %i.fa.fa-comment
= @notes_count = @notes_count
...@@ -23,11 +23,6 @@ ...@@ -23,11 +23,6 @@
%p %p
.commit-info-row .commit-info-row
- if @commit.status
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
build:
= ci_label_for_status(@commit.status)
%span.light Authored by %span.light Authored by
%strong %strong
= commit_author_link(@commit, avatar: true, size: 24) = commit_author_link(@commit, avatar: true, size: 24)
...@@ -51,6 +46,17 @@ ...@@ -51,6 +46,17 @@
%span.commit-info.branches %span.commit-info.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
- if @commit.status
.commit-info-row
Builds for
= pluralize(@commit.ci_commits.count, 'pipeline')
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
= ci_label_for_status(@commit.status)
- if @commit.ci_commits.duration
in
= time_interval_in_words @commit.ci_commits.duration
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line = markdown escape_once(@commit.title), pipeline: :single_line
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.pull-right .pull-right
- if commit.status - if commit.status
= render_ci_status(commit) = render_commit_status(commit)
= clipboard_button(clipboard_text: commit.id) = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
.diff-content.diff-wrap-lines .diff-content.diff-wrap-lines
- # Skip all non non-supported blobs - # Skip all non non-supported blobs
- return unless blob.respond_to?('text?') - return unless blob.respond_to?(:text?)
- if diff_file.too_large? - if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large. .nothing-here-block This diff could not be displayed because it is too large.
- elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob)
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
- else - else
%strong ##{generic_commit_status.id} %strong ##{generic_commit_status.id}
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
- if defined?(commit_sha) && commit_sha - if defined?(commit_sha) && commit_sha
%td %td
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
...@@ -42,13 +45,19 @@ ...@@ -42,13 +45,19 @@
- generic_commit_status.tags.each do |tag| - generic_commit_status.tags.each do |tag|
%span.label.label-primary %span.label.label-primary
= tag = tag
- if defined?(retried) && retried
%span.label.label-warning retried
%td.duration %td.duration
- if generic_commit_status.duration - if generic_commit_status.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)} #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
%td.timestamp %td.timestamp
- if generic_commit_status.finished_at - if generic_commit_status.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)} %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage - if defined?(coverage) && coverage
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%li %li
%span.merge-request-ci-status %span.merge-request-ci-status
- if merge_request.ci_commit - if merge_request.ci_commit
= render_ci_status(merge_request.ci_commit) = render_pipeline_status(merge_request.ci_commit)
- elsif has_any_ci - elsif has_any_ci
= icon('blank fw') = icon('blank fw')
%span.merge-request-id %span.merge-request-id
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- ci_commit = @project.ci_commit(sha, branch) if sha - ci_commit = @project.ci_commit(sha, branch) if sha
- if ci_commit - if ci_commit
%span.related-branch-ci-status %span.related-branch-ci-status
= render_ci_status(ci_commit) = render_pipeline_status(ci_commit)
%span.related-branch-info %span.related-branch-info
%strong %strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- if merge_request.ci_commit - if merge_request.ci_commit
%li %li
= render_ci_status(merge_request.ci_commit) = render_pipeline_status(merge_request.ci_commit)
- if merge_request.open? && merge_request.broken? - if merge_request.open? && merge_request.broken?
%li %li
......
- header_title project_title(@project, "Pipelines", project_pipelines_path(@project))
%p
.commit-info-row
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
with
= pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref
for
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
= time_interval_in_words @pipeline.duration
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
= ci_icon_for_status(@pipeline.status)
= ci_label_for_status(@pipeline.status)
- if @commit
.commit-info-row
%span.light Authored by
%strong
= commit_author_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.authored_date)}
.commit-info-row
%span.light Commit
= link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
= clipboard_button(clipboard_text: @pipeline.sha)
- if @commit
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
- page_title "Pipelines"
= render "header_title"
.top-area
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
%li{class: ('active' if @scope == 'running')}
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
%li{class: ('active' if @scope == 'branches')}
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
%li{class: ('active' if @scope == 'tags')}
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
.nav-controls
- if can? current_user, :create_pipeline, @project
= link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
= icon('plus')
New pipeline
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint
.row-content-block
- if @scope == 'running'
Running pipelines for this project
- elsif @scope.nil?
Pipelines for this project
- else
#{@scope.titleize} for this project
%ul.content-list
- stages = @pipelines.stages
- if @pipelines.blank?
%li
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.builds
%tbody
%th ID
%th Commit
- stages.each do |stage|
%th
%span.pipeline-stage.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize.pluralize
%th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
- page_title "New Pipeline"
= render "header_title"
%h3.page-title
New Pipeline
%hr
= form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
= f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
var availableRefs = #{@project.repository.ref_names.to_json};
new NewBranchForm($('.js-new-pipeline-form'), availableRefs)
- page_title "Pipeline"
= render "header_title"
.prepend-top-default
- if @commit
= render "projects/pipelines/info"
%div.block-connector
= render "projects/commit/ci_commit", ci_commit: @pipeline
= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| = form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f|
= form_errors(runner)
.form-group .form-group
= label :active, "Active", class: 'control-label' = label :active, "Active", class: 'control-label'
.col-sm-10 .col-sm-10
.checkbox .checkbox
= f.check_box :active = f.check_box :active
%span.light Paused runners don't accept new builds %span.light Paused runners don't accept new builds
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
.form-group .form-group
= label_tag :token, class: 'control-label' do = label_tag :token, class: 'control-label' do
Token Token
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- if @runners.include?(runner) - if @runners.include?(runner)
= link_to runner.short_sha, runner_path(runner) = link_to runner.short_sha, runner_path(runner)
%small %small
=link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn %i.fa.fa-edit.btn
- else - else
= runner.short_sha = runner.short_sha
......
- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" - page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
%h4 Runner ##{@runner.id} %h4 Runner ##{@runner.id}
%hr %hr
= render 'form', runner: @runner, runner_form_url: runner_path(@runner) = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
...@@ -17,50 +17,39 @@ ...@@ -17,50 +17,39 @@
%th Property Name %th Property Name
%th Value %th Value
%tr %tr
%td %td Active
Tags %td= @runner.active? ? 'Yes' : 'No'
%tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
%td Tags
%td %td
- @runner.tag_list.each do |tag| - @runner.tag_list.each do |tag|
%span.label.label-primary %span.label.label-primary
= tag = tag
%tr %tr
%td %td Name
Name %td= @runner.name
%td
= @runner.name
%tr %tr
%td %td Version
Version %td= @runner.version
%td
= @runner.version
%tr %tr
%td %td Revision
Revision %td= @runner.revision
%td
= @runner.revision
%tr %tr
%td %td Platform
Platform %td= @runner.platform
%td
= @runner.platform
%tr %tr
%td %td Architecture
Architecture %td= @runner.architecture
%td
= @runner.architecture
%tr %tr
%td %td Description
Description %td= @runner.description
%td
= @runner.description
%tr %tr
%td %td Last contact
Last contact
%td %td
- if @runner.contacted_at - if @runner.contacted_at
#{time_ago_in_words(@runner.contacted_at)} ago #{time_ago_in_words(@runner.contacted_at)} ago
- else - else
Never Never
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= project.main_language = project.main_language
- if project.commit.try(:status) - if project.commit.try(:status)
%span %span
= render_ci_status(project.commit) = render_commit_status(project.commit)
- if forks - if forks
%span %span
= icon('code-fork') = icon('code-fork')
......
...@@ -27,15 +27,18 @@ class EmailsOnPushWorker ...@@ -27,15 +27,18 @@ class EmailsOnPushWorker
:push :push
end end
diff_refs = nil
compare = nil compare = nil
reverse_compare = false reverse_compare = false
if action == :push if action == :push
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)]
return false if compare.same return false if compare.same
if compare.commits.empty? if compare.commits.empty?
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)]
reverse_compare = true reverse_compare = true
...@@ -53,6 +56,7 @@ class EmailsOnPushWorker ...@@ -53,6 +56,7 @@ class EmailsOnPushWorker
action: action, action: action,
compare: compare, compare: compare,
reverse_compare: reverse_compare, reverse_compare: reverse_compare,
diff_refs: diff_refs,
send_from_committer_email: send_from_committer_email, send_from_committer_email: send_from_committer_email,
disable_diffs: disable_diffs disable_diffs: disable_diffs
) )
......
...@@ -26,6 +26,8 @@ module Gitlab ...@@ -26,6 +26,8 @@ module Gitlab
#{config.root}/app/models/members #{config.root}/app/models/members
#{config.root}/app/models/project_services)) #{config.root}/app/models/project_services))
config.generators.templates.push("#{config.root}/generator_templates")
# Only load the plugins named here, in the order given (default is alphabetical). # Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named. # :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ] # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
...@@ -78,6 +80,7 @@ module Gitlab ...@@ -78,6 +80,7 @@ module Gitlab
config.assets.precompile << "*.png" config.assets.precompile << "*.png"
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
config.assets.precompile << "notify.css" config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/repository_push_email.css"
# Version of your assets, change this if you want to expire all your assets # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
...@@ -666,6 +666,13 @@ Rails.application.routes.draw do ...@@ -666,6 +666,13 @@ Rails.application.routes.draw do
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy] resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
member do
post :cancel
post :retry
end
end
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do collection do
post :cancel_all post :cancel_all
......
class AddRunUntaggedToCiRunner < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_column_with_default(:ci_runners, :run_untagged, :boolean,
default: true, allow_null: false)
end
def down
remove_column(:ci_runners, :run_untagged)
end
end
...@@ -269,6 +269,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do ...@@ -269,6 +269,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do
t.string "revision" t.string "revision"
t.string "platform" t.string "platform"
t.string "architecture" t.string "architecture"
t.boolean "run_untagged", default: true, null: false
end end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
......
...@@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run. ...@@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run.
For instance, at GitLab we have runners tagged with "rails" if they contain For instance, at GitLab we have runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites. the appropriate dependencies to run Rails test suites.
### Be Careful with Sensitive Information ### Prevent runner with tags from picking jobs without tags
You can configure a runner to prevent it from picking jobs with tags when
the runnner does not have tags assigned. This setting is available on each
runner in *Project Settings* > *Runners*.
### Be careful with sensitive information
If you can run a build on a runner, you can get access to any code it runs If you can run a build on a runner, you can get access to any code it runs
and get the token of the runner. With shared runners, this means that anyone and get the token of the runner. With shared runners, this means that anyone
......
...@@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ...@@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script ### after_script
>**Note:** >**Note:**
Introduced in GitLab 8.7 and GitLab Runner v1.2. Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released)
`after_script` is used to define the command that will be run after for all `after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string. builds. This has to be an array or a multi-line string.
......
...@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ...@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end end
step 'I should see "Shop" project CI status' do step 'I should see "Shop" project CI status' do
expect(page).to have_link "Build skipped" expect(page).to have_link "Commit: skipped"
end end
step 'I should see last push widget' do step 'I should see last push widget' do
......
...@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I see commit ci info' do step 'I see commit ci info' do
expect(page).to have_content "build: pending" expect(page).to have_content "Builds for 1 pipeline pending"
end end
step 'I click status link' do step 'I click status link' do
...@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I see builds list' do step 'I see builds list' do
expect(page).to have_content "build: pending" expect(page).to have_content "Builds for 1 pipeline pending"
expect(page).to have_content "1 build" expect(page).to have_content "1 build"
end end
......
...@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see merge request "Bug NS-05" with CI status' do step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do page.within ".mr-list" do
expect(page).to have_link "Build pending" expect(page).to have_link "Pipeline: pending"
end end
end end
......
...@@ -408,6 +408,7 @@ module API ...@@ -408,6 +408,7 @@ module API
class RunnerDetails < Runner class RunnerDetails < Runner
expose :tag_list expose :tag_list
expose :run_untagged
expose :version, :revision, :platform, :architecture expose :version, :revision, :platform, :architecture
expose :contacted_at expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
......
...@@ -49,7 +49,7 @@ module API ...@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id]) runner = get_runner(params[:id])
authenticate_update_runner!(runner) authenticate_update_runner!(runner)
attrs = attributes_for_keys [:description, :active, :tag_list] attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged]
if runner.update(attrs) if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user present runner, with: Entities::RunnerDetails, current_user: current_user
else else
......
...@@ -28,20 +28,20 @@ module Ci ...@@ -28,20 +28,20 @@ module Ci
post "register" do post "register" do
required_attributes! [:token] required_attributes! [:token]
attributes = { description: params[:description],
tag_list: params[:tag_list] }
unless params[:run_untagged].nil?
attributes[:run_untagged] = params[:run_untagged]
end
runner = runner =
if runner_registration_token_valid? if runner_registration_token_valid?
# Create shared runner. Requires admin access # Create shared runner. Requires admin access
Ci::Runner.create( Ci::Runner.create(attributes.merge(is_shared: true))
description: params[:description],
tag_list: params[:tag_list],
is_shared: true
)
elsif project = Project.find_by(runners_token: params[:token]) elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project. # Create a specific runner for project.
project.runners.create( project.runners.create(attributes)
description: params[:description],
tag_list: params[:tag_list]
)
end end
return forbidden! unless runner return forbidden! unless runner
......
...@@ -47,7 +47,7 @@ module Gitlab ...@@ -47,7 +47,7 @@ module Gitlab
first['count']. first['count'].
to_i to_i
# Update in batches of 5% with an upper limit of 5000 rows. # Update in batches of 5%
batch_size = ((total / 100.0) * 5.0).ceil batch_size = ((total / 100.0) * 5.0).ceil
while processed < total while processed < total
......
...@@ -5,6 +5,7 @@ module Gitlab ...@@ -5,6 +5,7 @@ module Gitlab
attr_reader :author_id, :ref, :action attr_reader :author_id, :ref, :action
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
include DiffHelper
delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author delegate :name, to: :author, prefix: :author
...@@ -36,7 +37,7 @@ module Gitlab ...@@ -36,7 +37,7 @@ module Gitlab
end end
def diffs def diffs
@diffs ||= (compare.diffs if compare) @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare)
end end
def diffs_count def diffs_count
...@@ -47,6 +48,10 @@ module Gitlab ...@@ -47,6 +48,10 @@ module Gitlab
@opts[:compare] @opts[:compare]
end end
def diff_refs
@opts[:diff_refs]
end
def compare_timeout def compare_timeout
diffs.overflow? if diffs diffs.overflow? if diffs
end end
......
...@@ -18,5 +18,9 @@ FactoryGirl.define do ...@@ -18,5 +18,9 @@ FactoryGirl.define do
commit_id RepoHelpers.sample_commit.id commit_id RepoHelpers.sample_commit.id
target_type "Commit" target_type "Commit"
end end
trait :build_failed do
action { Todo::BUILD_FAILED }
end
end end
end end
require 'spec_helper'
describe "Pipelines" do
include GitlabRoutingHelper
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
before do
login_as(user)
project.team << [user, :developer]
end
describe 'GET /:project/pipelines' do
let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') }
[:all, :running, :branches].each do |scope|
context "displaying #{scope}" do
let(:project) { create(:project) }
before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
it { expect(page).to have_content(pipeline.short_sha) }
end
end
context 'cancelable pipeline' do
let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Cancel') }
it { expect(page).to have_selector('.ci-running') }
context 'when canceling' do
before { click_link('Cancel') }
it { expect(page).to_not have_link('Cancel') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
context 'retryable pipelines' do
let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Retry') }
it { expect(page).to have_selector('.ci-failed') }
context 'when retrying' do
before { click_link('Retry') }
it { expect(page).to_not have_link('Retry') }
it { expect(page).to have_selector('.ci-pending') }
end
end
context 'downloadable pipelines' do
context 'with artifacts' do
let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') }
it { expect(page).to have_link(with_artifacts.name) }
end
context 'without artifacts' do
let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') }
it { expect(page).to_not have_selector('.build-artifacts') }
end
end
end
describe 'GET /:project/pipelines/:id' do
let(:pipeline) { create(:ci_commit, project: project, ref: 'master') }
before do
@success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy')
@external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external')
end
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
it 'showing a list of builds' do
expect(page).to have_content('Tests')
expect(page).to have_content(@success.id)
expect(page).to have_content('Deploy')
expect(page).to have_content(@failed.id)
expect(page).to have_content(@running.id)
expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
end
context 'retrying builds' do
it { expect(page).to_not have_content('retried') }
context 'when retrying' do
before { click_on 'Retry failed' }
it { expect(page).to_not have_content('Retry failed') }
it { expect(page).to have_content('retried') }
end
end
context 'canceling builds' do
it { expect(page).to_not have_selector('.ci-canceled') }
context 'when canceling' do
before { click_on 'Cancel running' }
it { expect(page).to_not have_content('Cancel running') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
end
describe 'POST /:project/pipelines' do
let(:project) { create(:project) }
before { visit new_namespace_project_pipeline_path(project.namespace, project) }
context 'for valid commit' do
before { fill_in('Create for', with: 'master') }
context 'with gitlab-ci.yml' do
before { stub_ci_commit_to_return_yaml_file }
it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) }
end
context 'without gitlab-ci.yml' do
before { click_on 'Create pipeline' }
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
context 'for invalid commit' do
before do
fill_in('Create for', with: 'invalid reference')
click_on 'Create pipeline'
end
it { expect(page).to have_content('Reference not found') }
end
end
end
...@@ -110,4 +110,37 @@ describe "Runners" do ...@@ -110,4 +110,37 @@ describe "Runners" do
expect(page).to have_content(@specific_runner.platform) expect(page).to have_content(@specific_runner.platform)
end end
end end
feature 'configuring runners ability to picking untagged jobs' do
given(:project) { create(:empty_project) }
given(:runner) { create(:ci_runner) }
background do
project.team << [user, :master]
project.runners << runner
end
scenario 'user checks default configuration' do
visit namespace_project_runner_path(project.namespace, project, runner)
expect(page).to have_content 'Can run untagged jobs Yes'
end
context 'when runner has tags' do
before { runner.update_attribute(:tag_list, ['tag']) }
scenario 'user wants to prevent runner from running untagged job' do
visit runners_path(project)
page.within('.activated-specific-runners') do
first('small > a').click
end
uncheck 'runner_run_untagged'
click_button 'Save changes'
expect(page).to have_content 'Can run untagged jobs No'
expect(runner.reload.run_untagged?).to eq false
end
end
end
end end
...@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do ...@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs' do describe '#diffs' do
subject { message.diffs } subject { message.diffs }
it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) } it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
end end
describe '#diffs_count' do describe '#diffs_count' do
......
...@@ -693,8 +693,9 @@ describe Notify do ...@@ -693,8 +693,9 @@ describe Notify do
let(:commits) { Commit.decorate(compare.commits, nil) } let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false } let(:send_from_committer_email) { false }
let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link" it_behaves_like "a user cannot unsubscribe through footer link"
...@@ -715,15 +716,15 @@ describe Notify do ...@@ -715,15 +716,15 @@ describe Notify do
is_expected.to have_body_text /Change some files/ is_expected.to have_body_text /Change some files/
end end
it 'includes diffs' do it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text /def archive_formats_regex/ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end end
it 'contains a link to the diff' do it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/ is_expected.to have_body_text /#{diff_path}/
end end
it 'doesn not contain the misleading footer' do it 'does not contain the misleading footer' do
is_expected.not_to have_body_text /you are a member of/ is_expected.not_to have_body_text /you are a member of/
end end
...@@ -797,8 +798,9 @@ describe Notify do ...@@ -797,8 +798,9 @@ describe Notify do
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
let(:commits) { Commit.decorate(compare.commits, nil) } let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link" it_behaves_like "a user cannot unsubscribe through footer link"
...@@ -819,8 +821,8 @@ describe Notify do ...@@ -819,8 +821,8 @@ describe Notify do
is_expected.to have_body_text /Change some files/ is_expected.to have_body_text /Change some files/
end end
it 'includes diffs' do it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text /def archive_formats_regex/ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end end
it 'contains a link to the diff' do it 'contains a link to the diff' do
......
...@@ -259,11 +259,11 @@ describe Ci::Build, models: true do ...@@ -259,11 +259,11 @@ describe Ci::Build, models: true do
end end
describe '#can_be_served?' do describe '#can_be_served?' do
let(:runner) { FactoryGirl.create :ci_runner } let(:runner) { create(:ci_runner) }
before { build.project.runners << runner } before { build.project.runners << runner }
context 'runner without tags' do context 'when runner does not have tags' do
it 'can handle builds without tags' do it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy expect(build.can_be_served?(runner)).to be_truthy
end end
...@@ -274,23 +274,51 @@ describe Ci::Build, models: true do ...@@ -274,23 +274,51 @@ describe Ci::Build, models: true do
end end
end end
context 'runner with tags' do context 'when runner has tags' do
before { runner.tag_list = ['bb', 'cc'] } before { runner.tag_list = ['bb', 'cc'] }
it 'can handle builds without tags' do shared_examples 'tagged build picker' do
expect(build.can_be_served?(runner)).to be_truthy
end
it 'can handle build with matching tags' do it 'can handle build with matching tags' do
build.tag_list = ['bb'] build.tag_list = ['bb']
expect(build.can_be_served?(runner)).to be_truthy expect(build.can_be_served?(runner)).to be_truthy
end end
it 'cannot handle build with not matching tags' do it 'cannot handle build without matching tags' do
build.tag_list = ['aa'] build.tag_list = ['aa']
expect(build.can_be_served?(runner)).to be_falsey expect(build.can_be_served?(runner)).to be_falsey
end end
end end
context 'when runner can pick untagged jobs' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
it_behaves_like 'tagged build picker'
end
context 'when runner can not pick untagged jobs' do
before { runner.run_untagged = false }
it 'can not handle builds without tags' do
expect(build.can_be_served?(runner)).to be_falsey
end
it_behaves_like 'tagged build picker'
end
end
end
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
subject { create(:ci_build, tag_list: []) }
it { is_expected.to_not have_tags }
end
end end
describe '#any_runners_online?' do describe '#any_runners_online?' do
......
...@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do ...@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do
it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:builds) }
it { is_expected.to validate_presence_of :sha } it { is_expected.to validate_presence_of :sha }
it { is_expected.to validate_presence_of :status } it { is_expected.to validate_presence_of :status }
it { is_expected.to delegate_method(:stages).to(:statuses) }
it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :git_author_email }
......
require 'spec_helper' require 'spec_helper'
describe Ci::Runner, models: true do describe Ci::Runner, models: true do
describe 'validation' do
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
it 'is not valid' do
runner = build(:ci_runner, tag_list: [], run_untagged: false)
expect(runner).to be_invalid
end
end
context 'when runner has tags' do
it 'is valid' do
runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false)
expect(runner).to be_valid
end
end
end
end
describe '#display_name' do describe '#display_name' do
it 'should return the description if it has a value' do it 'should return the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
...@@ -114,7 +132,19 @@ describe Ci::Runner, models: true do ...@@ -114,7 +132,19 @@ describe Ci::Runner, models: true do
end end
end end
describe '#search' do describe '#has_tags?' do
context 'when runner has tags' do
subject { create(:ci_runner, tag_list: ['tag']) }
it { is_expected.to have_tags }
end
context 'when runner does not have tags' do
subject { create(:ci_runner, tag_list: []) }
it { is_expected.to_not have_tags }
end
end
describe '.search' do
let(:runner) { create(:ci_runner, token: '123abc') } let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do it 'returns runners with a matching token' do
......
...@@ -184,21 +184,24 @@ describe API::Runners, api: true do ...@@ -184,21 +184,24 @@ describe API::Runners, api: true do
description = shared_runner.description description = shared_runner.description
active = shared_runner.active active = shared_runner.active
put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active, update_runner(shared_runner.id, admin, description: "#{description}_updated",
tag_list: ['ruby2.1', 'pgsql', 'mysql'] active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false')
shared_runner.reload shared_runner.reload
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active) expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be false
end end
end end
context 'when runner is not shared' do context 'when runner is not shared' do
it 'should update runner' do it 'should update runner' do
description = specific_runner.description description = specific_runner.description
put api("/runners/#{specific_runner.id}", admin), description: 'test' update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload specific_runner.reload
expect(response.status).to eq(200) expect(response.status).to eq(200)
...@@ -208,10 +211,14 @@ describe API::Runners, api: true do ...@@ -208,10 +211,14 @@ describe API::Runners, api: true do
end end
it 'should return 404 if runner does not exists' do it 'should return 404 if runner does not exists' do
put api('/runners/9999', admin), description: 'test' update_runner(9999, admin, description: 'test')
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
def update_runner(id, user, args)
put api("/runners/#{id}", user), args
end
end end
context 'authorized user' do context 'authorized user' do
......
...@@ -128,6 +128,38 @@ describe Ci::API::API do ...@@ -128,6 +128,38 @@ describe Ci::API::API do
end end
end end
end end
context 'when build has no tags' do
before do
commit = create(:ci_commit, project: project)
create(:ci_build, commit: commit, tags: [])
end
context 'when runner is allowed to pick untagged builds' do
before { runner.update_column(:run_untagged, true) }
it 'picks build' do
register_builds
expect(response).to have_http_status 201
end
end
context 'when runner is not allowed to pick untagged builds' do
before { runner.update_column(:run_untagged, false) }
it 'does not pick build' do
register_builds
expect(response).to have_http_status 404
end
end
def register_builds
post ci_api("/builds/register"), token: runner.token,
info: { platform: :darwin }
end
end
end end
describe "PUT /builds/:id" do describe "PUT /builds/:id" do
......
...@@ -12,44 +12,85 @@ describe Ci::API::API do ...@@ -12,44 +12,85 @@ describe Ci::API::API do
end end
describe "POST /runners/register" do describe "POST /runners/register" do
describe "should create a runner if token provided" do context 'when runner token is provided' do
before { post ci_api("/runners/register"), token: registration_token } before { post ci_api("/runners/register"), token: registration_token }
it { expect(response.status).to eq(201) } it 'creates runner with default values' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be true
end
end end
describe "should create a runner with description" do context 'when runner description is provided' do
before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" } before do
post ci_api("/runners/register"), token: registration_token,
description: "server.hostname"
end
it { expect(response.status).to eq(201) } it 'creates runner' do
it { expect(Ci::Runner.first.description).to eq("server.hostname") } expect(response).to have_http_status 201
expect(Ci::Runner.first.description).to eq("server.hostname")
end
end end
describe "should create a runner with tags" do context 'when runner tags are provided' do
before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" } before do
post ci_api("/runners/register"), token: registration_token,
tag_list: "tag1, tag2"
end
it { expect(response.status).to eq(201) } it 'creates runner' do
it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) } expect(response).to have_http_status 201
expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
end
end end
describe "should create a runner if project token provided" do context 'when option for running untagged jobs is provided' do
context 'when tags are provided' do
it 'creates runner' do
post ci_api("/runners/register"), token: registration_token,
run_untagged: false,
tag_list: ['tag']
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be false
end
end
context 'when tags are not provided' do
it 'does not create runner' do
post ci_api("/runners/register"), token: registration_token,
run_untagged: false
expect(response).to have_http_status 404
end
end
end
context 'when project token is provided' do
let(:project) { FactoryGirl.create(:empty_project) } let(:project) { FactoryGirl.create(:empty_project) }
before { post ci_api("/runners/register"), token: project.runners_token } before { post ci_api("/runners/register"), token: project.runners_token }
it { expect(response.status).to eq(201) } it 'creates runner' do
it { expect(project.runners.size).to eq(1) } expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
end
end end
it "should return 403 error if token is invalid" do context 'when token is invalid' do
it 'returns 403 error' do
post ci_api("/runners/register"), token: 'invalid' post ci_api("/runners/register"), token: 'invalid'
expect(response.status).to eq(403) expect(response).to have_http_status 403
end
end end
it "should return 400 error if no token" do context 'when no token provided' do
it 'returns 400 error' do
post ci_api("/runners/register") post ci_api("/runners/register")
expect(response.status).to eq(400) expect(response).to have_http_status 400
end
end end
%w(name version revision platform architecture).each do |param| %w(name version revision platform architecture).each do |param|
...@@ -60,7 +101,7 @@ describe Ci::API::API do ...@@ -60,7 +101,7 @@ describe Ci::API::API do
it do it do
post ci_api("/runners/register"), token: registration_token, info: { param => value } post ci_api("/runners/register"), token: registration_token, info: { param => value }
expect(response.status).to eq(201) expect(response).to have_http_status 201
is_expected.to eq(value) is_expected.to eq(value)
end end
end end
...@@ -71,7 +112,7 @@ describe Ci::API::API do ...@@ -71,7 +112,7 @@ describe Ci::API::API do
let!(:runner) { FactoryGirl.create(:ci_runner) } let!(:runner) { FactoryGirl.create(:ci_runner) }
before { delete ci_api("/runners/delete"), token: runner.token } before { delete ci_api("/runners/delete"), token: runner.token }
it { expect(response.status).to eq(200) } it { expect(response).to have_http_status 200 }
it { expect(Ci::Runner.count).to eq(0) } it { expect(Ci::Runner.count).to eq(0) }
end end
end end
require 'spec_helper'
# Write specs in this file.
describe MergeRequests::AddTodoWhenBuildFailsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { create(:project) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
let(:ci_commit) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
let(:todo_service) { TodoService.new }
let(:merge_request) do
create(:merge_request, merge_user: user, source_branch: 'master',
target_branch: 'feature', source_project: project, target_project: project,
state: 'opened')
end
before do
allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
allow(service).to receive(:todo_service).and_return(todo_service)
end
describe '#execute' do
context 'commit status with ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) }
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
service.execute(commit_status)
end
end
context 'commit status with non-HEAD ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
service.execute(commit_status)
end
end
context 'commit status without ref' do
let(:commit_status) { create(:generic_commit_status) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
service.execute(commit_status)
end
end
end
describe '#close' do
context 'commit status with ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) }
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
service.close(commit_status)
end
end
context 'commit status with non-HEAD ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_retried)
service.close(commit_status)
end
end
context 'commit status without ref' do
let(:commit_status) { create(:generic_commit_status) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_retried)
service.close(commit_status)
end
end
end
end
...@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do ...@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do
target_branch: 'feature', target_branch: 'feature',
target_project: @project) target_project: @project)
@build_failed_todo = create(:todo,
:build_failed,
user: @user,
project: @project,
target: @merge_request,
author: @user)
@fork_build_failed_todo = create(:todo,
:build_failed,
user: @user,
project: @project,
target: @merge_request,
author: @user)
@commits = @merge_request.commits @commits = @merge_request.commits
@oldrev = @commits.last.id @oldrev = @commits.last.id
...@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey} it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_done }
end end
context 'push to origin repo target branch' do context 'push to origin repo target branch' do
...@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged } it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end end
context 'manual merge of source branch' do context 'manual merge of source branch' do
...@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end end
context 'push to fork repo source branch' do context 'push to fork repo source branch' do
...@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open } it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end end
context 'push to fork repo target branch' do context 'push to fork repo target branch' do
...@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open } it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end end
context 'push to origin repo target branch after fork project was removed' do context 'push to origin repo target branch after fork project was removed' do
...@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged } it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end end
context 'push new branch that exists in a merge request' do context 'push new branch that exists in a merge request' do
...@@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do
def reload_mrs def reload_mrs
@merge_request.reload @merge_request.reload
@fork_merge_request.reload @fork_merge_request.reload
@build_failed_todo.reload
@fork_build_failed_todo.reload
end end
end end
end end
...@@ -305,6 +305,25 @@ describe TodoService, services: true do ...@@ -305,6 +305,25 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done expect(second_todo.reload).to be_done
end end
end end
describe '#merge_request_build_failed' do
it 'creates a pending todo for the merge request author' do
service.merge_request_build_failed(mr_unassigned)
should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
end
end
describe '#merge_request_push' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe)
second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe)
service.merge_request_push(mr_assigned, author)
expect(first_todo.reload).to be_done
expect(second_todo.reload).not_to be_done
end
end
end end
def should_create_todo(attributes = {}) def should_create_todo(attributes = {})
......
...@@ -48,6 +48,22 @@ describe PostReceive do ...@@ -48,6 +48,22 @@ describe PostReceive do
PostReceive.new.perform(pwd(project), key_id, base64_changes) PostReceive.new.perform(pwd(project), key_id, base64_changes)
end end
end end
context "gitlab-ci.yml" do
subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
context "creates a Ci::Commit for every change" do
before { stub_ci_commit_to_return_yaml_file }
it { expect{ subject }.to change{ Ci::Commit.count }.by(2) }
end
context "does not create a Ci::Commit" do
before { stub_ci_commit_yaml_file(nil) }
it { expect{ subject }.to_not change{ Ci::Commit.count } }
end
end
end end
context "webhook" do context "webhook" do
......
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