summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss43
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/controllers/projects/pipelines_controller.rb59
-rw-r--r--app/finders/pipelines_finder.rb38
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/ci_status_helper.rb29
-rw-r--r--app/helpers/emails_helper.rb6
-rw-r--r--app/helpers/todos_helper.rb8
-rw-r--r--app/mailers/emails/projects.rb3
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/ability.rb5
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/commit.rb24
-rw-r--r--app/models/commit_status.rb17
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/todo.rb9
-rw-r--r--app/services/ci/create_pipeline_service.rb50
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb17
-rw-r--r--app/services/merge_requests/base_service.rb25
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb23
-rw-r--r--app/services/merge_requests/refresh_service.rb7
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/views/dashboard/todos/_todo.html.haml12
-rw-r--r--app/views/layouts/nav/_project.html.haml7
-rw-r--r--app/views/layouts/notify.html.haml1
-rw-r--r--app/views/notify/repository_push_email.html.haml59
-rw-r--r--app/views/notify/repository_push_email.text.haml38
-rw-r--r--app/views/projects/ci/builds/_build.html.haml38
-rw-r--r--app/views/projects/ci/commits/_commit.html.haml77
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_ci_commit.html.haml70
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml14
-rw-r--r--app/views/projects/commit/_commit_box.html.haml18
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/deploy_keys/index.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml9
-rw-r--r--app/views/projects/hooks/index.html.haml2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/pipelines/_header_title.html.haml1
-rw-r--r--app/views/projects/pipelines/_info.html.haml37
-rw-r--r--app/views/projects/pipelines/index.html.haml66
-rw-r--r--app/views/projects/pipelines/new.html.haml22
-rw-r--r--app/views/projects/pipelines/show.html.haml9
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/triggers/index.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/workers/emails_on_push_worker.rb18
-rw-r--r--config/application.rb5
-rw-r--r--config/routes.rb7
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--features/steps/dashboard/dashboard.rb2
-rw-r--r--features/steps/project/commits/commits.rb4
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb (renamed from lib/templates/active_record/migration/create_table_migration.rb)0
-rw-r--r--generator_templates/active_record/migration/migration.rb (renamed from lib/templates/active_record/migration/migration.rb)0
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/email/message/repository_push.rb7
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/features/pipelines_spec.rb153
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb16
-rw-r--r--spec/models/ci/commit_spec.rb1
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb81
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb28
-rw-r--r--spec/services/todo_service_spec.rb19
71 files changed, 1057 insertions, 210 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 2d2055d76b5..0946ef5d848 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -21,7 +21,7 @@ AllCops:
- 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb'
- - 'lib/templates/**/*'
+ - 'generator_templates/**/*'
##################### Style ##################################
diff --git a/CHANGELOG b/CHANGELOG
index d0f3d037325..f07a4f65fd3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -46,7 +46,7 @@ v 8.8.0 (unreleased)
- Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes)
- Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724
- Added multiple colors for labels in dropdowns when dups happen.
- - Always group commits by server timezone, not commit timestamp
+ - Show commits in the same order as `git log`
- Improve description for the Two-factor Authentication sign-in screen. (Connor Shea)
- API support for the 'since' and 'until' operators on commit requests (Paco Guzman)
- Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko)
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
new file mode 100644
index 00000000000..001994db97b
--- /dev/null
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -0,0 +1,43 @@
+@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";
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
new file mode 100644
index 00000000000..546176b65e4
--- /dev/null
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -0,0 +1,4 @@
+.pipeline-stage {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
new file mode 100644
index 00000000000..b36081205d8
--- /dev/null
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -0,0 +1,59 @@
+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
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
new file mode 100644
index 00000000000..c19a795d467
--- /dev/null
+++ b/app/finders/pipelines_finder.rb
@@ -0,0 +1,38 @@
+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
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 3ba27c40504..4bd46a76087 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -36,7 +36,7 @@ class TodosFinder
private
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
def action_id
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 417050b4132..cfad17dcacf 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_ci_status(ci_commit, tooltip_placement: 'auto left')
- # TODO: split this method into
- # - render_commit_status
- # - render_pipeline_status
- link_to ci_icon_for_status(ci_commit.status),
- ci_status_path(ci_commit),
- class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
- title: "Build #{ci_label_for_status(ci_commit.status)}",
- data: { toggle: 'tooltip', placement: tooltip_placement }
+ def render_commit_status(commit, tooltip_placement: 'auto left')
+ project = commit.project
+ path = builds_namespace_project_commit_path(project.namespace, project, commit)
+ render_status_with_link('commit', commit.status, path, tooltip_placement)
+ end
+
+ def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
+ project = pipeline.project
+ path = namespace_project_pipeline_path(project.namespace, project, pipeline)
+ render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
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
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 41b5bd7be90..8466d0aa0ba 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -32,12 +32,6 @@ module EmailsHelper
nil
end
- def color_email_diff(diffcontent)
- formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
- lexer = Rouge::Lexers::Diff
- raw formatter.format(lexer.lex(diffcontent))
- end
-
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 2f066682180..81b9b5d7052 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -11,6 +11,7 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
+ when Todo::BUILD_FAILED then 'The build failed for your'
end
end
@@ -28,8 +29,11 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
- polymorphic_path([todo.project.namespace.becomes(Namespace),
- todo.project, todo.target], anchor: anchor)
+ path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
+
+ path.unshift(:builds) if todo.build_failed?
+
+ polymorphic_path(path, anchor: anchor)
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 5489283432b..fdf1e9f5afc 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -65,7 +65,8 @@ module Emails
# used in notify layout
@target_url = @message.target_url
- @project = Project.find project_id
+ @project = Project.find(project_id)
+ @diff_notes_disabled = true
add_project_headers
headers['X-GitLab-Author'] = @message.author_username
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 826e5f96fa1..1c663bdd521 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -10,6 +10,8 @@ class Notify < BaseMailer
include Emails::Builds
add_template_helper MergeRequestsHelper
+ add_template_helper DiffHelper
+ add_template_helper BlobHelper
add_template_helper EmailsHelper
def test_email(recipient_email, subject, body)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index f70268d3138..f7ea2fd2b1f 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -205,6 +205,7 @@ class Ability
:read_commit_status,
:read_build,
:read_container_image,
+ :read_pipeline,
]
end
@@ -216,6 +217,8 @@ class Ability
:update_commit_status,
:create_build,
:update_build,
+ :create_pipeline,
+ :update_pipeline,
:create_merge_request,
:create_wiki,
:push_code,
@@ -248,6 +251,7 @@ class Ability
:admin_commit_status,
:admin_build,
:admin_container_image,
+ :admin_pipeline
]
end
@@ -290,6 +294,7 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
+ rules += named_abilities('pipeline')
end
unless project.container_registry_enabled
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 92327bdb08d..50190101f2b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -53,6 +53,7 @@ module Ci
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
new_build.save
+ MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
end
end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index f4b61c75ab6..6675a3f5d53 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -8,8 +8,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
- delegate :stages, to: :statuses
-
validates_presence_of :sha
validates_presence_of :status
validate :valid_commit_sha
@@ -22,7 +20,8 @@ module Ci
end
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
def project_id
@@ -67,6 +66,25 @@ module Ci
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)
return unless config_processor
config_processor.stages.any? do |stage|
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cacbc13b391..f774b6e0efb 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user
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]) }
state_machine :status, initial: :pending do
@@ -45,6 +46,10 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
+
+ after_transition any => :failed do |commit_status|
+ MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
+ end
end
delegate :sha, :short_sha, to: :commit
@@ -54,13 +59,15 @@ class CommitStatus < ActiveRecord::Base
end
def self.stages
- order_by = 'max(stage_idx)'
- group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
+ # We group by stage name, but order stages by theirs' index
+ unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end
def self.stages_status
- all.stages.inject({}) do |h, stage|
- h[stage] = all.where(stage: stage).status
+ # We execute subquery for each stage to calculate a 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
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6ad8fc3f034..7d5103748f5 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -98,9 +98,7 @@ class MergeRequestDiff < ActiveRecord::Base
commits = compare.commits
if commits.present?
- commits = Commit.decorate(commits, merge_request.source_project).
- sort_by(&:created_at).
- reverse
+ commits = Commit.decorate(commits, merge_request.source_project).reverse
end
commits
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f8b59fe4126..3a091373329 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,6 +1,7 @@
class Todo < ActiveRecord::Base
- ASSIGNED = 1
- MENTIONED = 2
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
belongs_to :author, class_name: "User"
belongs_to :note
@@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base
state :done
end
+ def build_failed?
+ action == BUILD_FAILED
+ end
+
def body
if note.present?
note.note
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
new file mode 100644
index 00000000000..5bc0c31cb42
--- /dev/null
+++ b/app/services/ci/create_pipeline_service.rb
@@ -0,0 +1,50 @@
+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
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
new file mode 100644
index 00000000000..566049525cb
--- /dev/null
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -0,0 +1,17 @@
+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
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index e6837a18696..9d7fca6882d 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,5 +38,30 @@ module MergeRequests
def filter_params
super(:merge_request)
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
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb
index d6af12f9739..8fd6a4ea1f6 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -20,15 +20,9 @@ module MergeRequests
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status)
- merge_requests = merge_request_from(commit_status)
-
- merge_requests.each do |merge_request|
+ each_merge_request(commit_status) do |merge_request, ci_commit|
next unless merge_request.merge_when_build_succeeds?
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?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
@@ -47,20 +41,5 @@ module MergeRequests
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
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 8b3d56c2b4c..fe0579744b4 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -12,6 +12,7 @@ module MergeRequests
close_merge_requests
reload_merge_requests
reset_merge_when_build_succeeds
+ mark_pending_todos_done
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -80,6 +81,12 @@ module MergeRequests
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
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
if branch_added?
@commits = []
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 42c5bca90fd..4bf4e144727 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -80,6 +80,30 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
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:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -145,6 +169,12 @@ class TodoService
create_todos(mentioned_users, attributes)
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)
attributes = {
project_id: target.project.id,
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index aa0aff86d4d..539f1dc6036 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,13 +1,13 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
-
.todo-title.title
- %span.author-name
- - if todo.author
- = link_to_author(todo)
- - else
- (removed)
+ - unless todo.build_failed?
+ %span.author-name
+ - if todo.author
+ = link_to_author(todo)
+ - else
+ (removed)
%span.todo-label
= todo_action_name(todo)
- if todo.target
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index d3d715aad3b..a97fefcfb46 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -39,6 +39,13 @@
Commits
- 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
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw')
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 2997f59d946..dde2e2889dc 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -4,6 +4,7 @@
%title
GitLab
= stylesheet_link_tag 'notify'
+ = yield :head
%body
%div.content
= yield
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index f2e405b14fd..f1532371b2e 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,3 +1,6 @@
+= content_for :head do
+ = stylesheet_link_tag 'mailers/repository_push_email'
+
%h3
#{@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))}
@@ -43,26 +46,38 @@
= diff.new_path
- unless @message.disable_diffs?
- %h4 Changes:
- - @message.diffs.each_with_index do |diff, i|
- %li{id: "diff-#{i}"}
- %a{href: @message.target_url + "#diff-#{i}"}
- - if diff.deleted_file
- %strong
- = diff.old_path
- deleted
- - elsif diff.renamed_file
- %strong
- = diff.old_path
- &rarr;
- %strong
- = diff.new_path
- - else
- %strong
- = diff.new_path
- %hr
- = color_email_diff(diff.diff)
- %br
+ - diff_files = @message.diffs
- - if @message.compare_timeout
- %h5 Huge diff. To prevent performance issues changes are hidden
+ - if @message.compare_timeout
+ %h5 The diff was not included because it is too large.
+ - else
+ %h4 Changes:
+ - diff_files.each_with_index do |diff_file, i|
+ %li{id: "diff-#{i}"}
+ %a{href: @message.target_url + "#diff-#{i}"}<
+ - if diff_file.deleted_file
+ %strong<
+ = diff_file.old_path
+ deleted
+ - elsif diff_file.renamed_file
+ %strong<
+ = diff_file.old_path
+ &rarr;
+ %strong<
+ = 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
+ %hr
+ - 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
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 53869e36b28..5ac23aa3997 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -25,24 +25,28 @@
- else
\- #{diff.new_path}
- unless @message.disable_diffs?
- \
- \
- Changes:
- - @message.diffs.each do |diff|
+ - if @message.compare_timeout
\
- \=====================================
- - if diff.deleted_file
- #{diff.old_path} deleted
- - elsif diff.renamed_file
- #{diff.old_path} → #{diff.new_path}
- - else
- = diff.new_path
- \=====================================
- != diff.diff
- - if @message.compare_timeout
- \
- \
- Huge diff. To prevent performance issues it was hidden
+ \
+ The diff was not included because it is too large.
+ - else
+ \
+ \
+ Changes:
+ - @message.diffs.each do |diff_file|
+ \
+ \=====================================
+ - if diff_file.deleted_file
+ #{diff_file.old_path} deleted
+ - elsif diff_file.renamed_file
+ #{diff_file.old_path} → #{diff_file.new_path}
+ - else
+ = diff_file.new_path
+ \=====================================
+ - if diff_file.too_large?
+ The diff for this file was not included because it is too large.
+ - else
+ != diff_file.diff.diff
- if @message.target_url
\
\
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 8e95f040273..962b9fb2595 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -13,7 +13,9 @@
%strong ##{build.id}
- 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
%td
@@ -40,25 +42,29 @@
%td
= build.name
- %td
- .label-container
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.try(:trigger_request)
- %span.label.label-info triggered
- - if build.try(:allow_failure)
- %span.label.label-danger allowed to fail
- - if defined?(retried) && retried
- %span.label.label-warning retried
+ .pull-right
+ .label-container
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+ - if defined?(retried) && retried
+ %span.label.label-warning retried
%td.duration
- if build.duration
+ = icon("clock-o")
+ &nbsp;
#{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp
- if build.finished_at
+ = icon("calendar")
+ &nbsp;
%span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
@@ -70,11 +76,11 @@
.pull-right
- 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
- %i.fa.fa-download
+ = icon('download')
- if can?(current_user, :update_build, build)
- 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
- %i.fa.fa-remove.cred
+ = icon('remove', class: 'cred')
- 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
- %i.fa.fa-refresh
+ = icon('refresh')
diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml
new file mode 100644
index 00000000000..13162b41f9b
--- /dev/null
+++ b/app/views/projects/ci/commits/_commit.html.haml
@@ -0,0 +1,77 @@
+- 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")
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index 5c9a319edeb..7f7a15aa214 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,2 +1,2 @@
- @ci_commits.each do |ci_commit|
- = render "ci_commit", ci_commit: ci_commit
+ = render "ci_commit", ci_commit: ci_commit, pipeline_details: true
diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml
index e849aefb188..8228c067be0 100644
--- a/app/views/projects/commit/_ci_commit.html.haml
+++ b/app/views/projects/commit/_ci_commit.html.haml
@@ -1,24 +1,27 @@
.row-content-block.build-content.middle-block
.pull-right
- - if can?(current_user, :update_build, @project)
+ - if can?(current_user, :update_pipeline, @project)
- 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?
- = 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
- = pluralize ci_commit.statuses.count(:id), "build"
- - if ci_commit.ref
- for
- %span.label.label-info
- = ci_commit.ref
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
- - if ci_commit.duration
- in
- = time_interval_in_words ci_commit.duration
+ .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"
+ - if ci_commit.ref
+ for
+ = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
+ - if ci_commit.duration
+ in
+ = time_interval_in_words ci_commit.duration
- if ci_commit.yaml_errors.present?
.bs-callout.bs-callout-danger
@@ -34,38 +37,5 @@
.table-holder
%table.table.builds
- %thead
- %tr
- %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
+ - ci_commit.statuses.stages.each do |stage|
+ = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage)
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
new file mode 100644
index 00000000000..aaa318e1eb3
--- /dev/null
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -0,0 +1,14 @@
+%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;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 01163e526b2..028564c9305 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,6 +1,6 @@
.pull-right.commit-action-buttons
%div
- - if @notes_count > 0
+ - if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped
%i.fa.fa-comment
= @notes_count
@@ -23,11 +23,6 @@
%p
.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
%strong
= commit_author_link(@commit, avatar: true, size: 24)
@@ -51,6 +46,17 @@
%span.commit-info.branches
%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
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c7d8c9a0d15..655cb0ac3cb 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -17,7 +17,7 @@
.pull-right
- if commit.status
- = render_ci_status(commit)
+ = render_commit_status(commit)
= clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 82f39e59284..7283a78a64e 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -3,7 +3,7 @@
- commits, hidden = limited_commits(@commits)
-- commits.group_by { |c| c.committed_date.in_time_zone.to_date }.sort.reverse.each do |day, commits|
+- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
.row.commits-row
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
index e230834e8ba..04fbb37d93f 100644
--- a/app/views/projects/deploy_keys/index.html.haml
+++ b/app/views/projects/deploy_keys/index.html.haml
@@ -19,7 +19,7 @@
%ul.well-list
= render @enabled_keys
- else
- .profile-settings-message.text-center
+ .settings-message.text-center
No deploy keys found. Create one with the form above or add existing one below.
%h5.prepend-top-default
Deploy keys from projects you have access to (#{@available_project_keys.size})
@@ -27,7 +27,7 @@
%ul.well-list
= render @available_project_keys
- else
- .profile-settings-message.text-center
+ .settings-message.text-center
No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- if @available_public_keys.any?
%h5.prepend-top-default
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0f04fc5d33c..f10b5094eaf 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -41,7 +41,7 @@
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
- - return unless blob.respond_to?('text?')
+ - return unless blob.respond_to?(:text?)
- if diff_file.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)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index f21c864e35c..8129514964a 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -12,6 +12,9 @@
- else
%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
%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"
@@ -42,13 +45,19 @@
- generic_commit_status.tags.each do |tag|
%span.label.label-primary
= tag
+ - if defined?(retried) && retried
+ %span.label.label-warning retried
%td.duration
- if generic_commit_status.duration
+ = icon("clock-o")
+ &nbsp;
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
%td.timestamp
- if generic_commit_status.finished_at
+ = icon("calendar")
+ &nbsp;
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 36c1d69f060..cffe9a01a96 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -80,5 +80,5 @@
- @hooks.each do |hook|
= render "project_hook", hook: hook
- else
- %p.profile-settings-message.text-center.append-bottom-0
+ %p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index d6b38b327ff..e953353567e 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -7,7 +7,7 @@
%li
%span.merge-request-ci-status
- if merge_request.ci_commit
- = render_ci_status(merge_request.ci_commit)
+ = render_pipeline_status(merge_request.ci_commit)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index bdfa0c7009e..5f9d2919982 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -8,7 +8,7 @@
- ci_commit = @project.ci_commit(sha, branch) if sha
- if ci_commit
%span.related-branch-ci-status
- = render_ci_status(ci_commit)
+ = render_pipeline_status(ci_commit)
%span.related-branch-info
%strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 73c6a95f5ca..2c54171c6bd 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -13,7 +13,7 @@
- if merge_request.ci_commit
%li
- = render_ci_status(merge_request.ci_commit)
+ = render_pipeline_status(merge_request.ci_commit)
- if merge_request.open? && merge_request.broken?
%li
diff --git a/app/views/projects/pipelines/_header_title.html.haml b/app/views/projects/pipelines/_header_title.html.haml
new file mode 100644
index 00000000000..faf63d64a79
--- /dev/null
+++ b/app/views/projects/pipelines/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Pipelines", project_pipelines_path(@project))
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
new file mode 100644
index 00000000000..8289aefcde7
--- /dev/null
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -0,0 +1,37 @@
+%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))
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
new file mode 100644
index 00000000000..9d5b6d367c9
--- /dev/null
+++ b/app/views/projects/pipelines/index.html.haml
@@ -0,0 +1,66 @@
+- 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'
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
new file mode 100644
index 00000000000..b97c9f5f3b6
--- /dev/null
+++ b/app/views/projects/pipelines/new.html.haml
@@ -0,0 +1,22 @@
+- 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)
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
new file mode 100644
index 00000000000..b082d4d5da8
--- /dev/null
+++ b/app/views/projects/pipelines/show.html.haml
@@ -0,0 +1,9 @@
+- 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
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index b9e9dd8aaea..565905cbe7b 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,7 +1,7 @@
%h5.prepend-top-0
Already Protected (#{@branches.size})
- if @branches.empty?
- %p.profile-settings-message.text-center
+ %p.settings-message.text-center
No branches are protected, protect a branch with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f91885b216d..d73ac987161 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -18,7 +18,7 @@
%th
= render partial: 'trigger', collection: @triggers, as: :trigger
- else
- %p.profile-settings-message.text-center.append-bottom-default
+ %p.settings-message.text-center.append-bottom-default
There are no triggers to use, add one by the button below.
= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index ab8b022411d..9ef021747a5 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -17,7 +17,7 @@
= project.main_language
- if project.commit.try(:status)
%span
- = render_ci_status(project.commit)
+ = render_commit_status(project.commit)
- if forks
%span
= icon('code-fork')
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 6ebcba5f39b..fa959fc56e3 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -27,15 +27,18 @@ class EmailsOnPushWorker
:push
end
+ diff_refs = nil
compare = nil
reverse_compare = false
if action == :push
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
if compare.commits.empty?
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
@@ -48,13 +51,14 @@ class EmailsOnPushWorker
send_email(
recipient,
project_id,
- author_id: author_id,
- ref: ref,
- action: action,
- compare: compare,
- reverse_compare: reverse_compare,
- send_from_committer_email: send_from_committer_email,
- disable_diffs: disable_diffs
+ author_id: author_id,
+ ref: ref,
+ action: action,
+ compare: compare,
+ reverse_compare: reverse_compare,
+ diff_refs: diff_refs,
+ send_from_committer_email: send_from_committer_email,
+ disable_diffs: disable_diffs
)
# These are input errors and won't be corrected even if Sidekiq retries
diff --git a/config/application.rb b/config/application.rb
index cba80f38f1f..0e5a77285e5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,6 +26,8 @@ module Gitlab
#{config.root}/app/models/members
#{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).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
@@ -39,7 +41,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
- #
+ #
# Parameters filtered:
# - Password (:password, :password_confirmation)
# - Private tokens (:private_token)
@@ -78,6 +80,7 @@ module Gitlab
config.assets.precompile << "*.png"
config.assets.precompile << "print.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
config.assets.version = '1.0'
diff --git a/config/routes.rb b/config/routes.rb
index 18e62bc4455..d8a2435b078 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -666,6 +666,13 @@ Rails.application.routes.draw do
resources :variables, only: [:index, :show, :update, :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
collection do
post :cancel_all
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7e9bced7616..63866d8c71c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
>**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
builds. This has to be an array or a multi-line string.
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index b5980b35102..80ed4c6d64c 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I should see "Shop" project CI status' do
- expect(page).to have_link "Build skipped"
+ expect(page).to have_link "Commit: skipped"
end
step 'I should see last push widget' do
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 93c37bf507f..f33f37be951 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
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
step 'I click status link' do
@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
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"
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 3b1a00f628a..b79d19f1c58 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do
- expect(page).to have_link "Build pending"
+ expect(page).to have_link "Pipeline: pending"
end
end
diff --git a/lib/templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb
index 27acc75dcc4..27acc75dcc4 100644
--- a/lib/templates/active_record/migration/create_table_migration.rb
+++ b/generator_templates/active_record/migration/create_table_migration.rb
diff --git a/lib/templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb
index 06bdea11367..06bdea11367 100644
--- a/lib/templates/active_record/migration/migration.rb
+++ b/generator_templates/active_record/migration/migration.rb
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 31a87b73fbe..9b662d163f0 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -47,7 +47,7 @@ module Gitlab
first['count'].
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
while processed < total
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 2c91a0487c3..e2fee6b9f3e 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -5,6 +5,7 @@ module Gitlab
attr_reader :author_id, :ref, :action
include Gitlab::Routing.url_helpers
+ include DiffHelper
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
@@ -36,7 +37,7 @@ module Gitlab
end
def diffs
- @diffs ||= (compare.diffs if compare)
+ @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare)
end
def diffs_count
@@ -47,6 +48,10 @@ module Gitlab
@opts[:compare]
end
+ def diff_refs
+ @opts[:diff_refs]
+ end
+
def compare_timeout
diffs.overflow? if diffs
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index e3681ae93a5..f426e27afed 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -18,5 +18,9 @@ FactoryGirl.define do
commit_id RepoHelpers.sample_commit.id
target_type "Commit"
end
+
+ trait :build_failed do
+ action { Todo::BUILD_FAILED }
+ end
end
end
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
new file mode 100644
index 00000000000..32665aadd22
--- /dev/null
+++ b/spec/features/pipelines_spec.rb
@@ -0,0 +1,153 @@
+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
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 7d6cce6daec..c19f33e2224 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs' do
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
describe '#diffs_count' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 5f7e4a526e6..b963a3e0324 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -693,8 +693,9 @@ describe Notify do
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(: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 "a user cannot unsubscribe through footer link"
@@ -715,15 +716,15 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/
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/
end
@@ -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(:commits) { Commit.decorate(compare.commits, nil) }
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 "a user cannot unsubscribe through footer link"
@@ -819,8 +821,8 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index dc071ad1c90..1b5940ad5a8 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do
it { is_expected.to have_many(:builds) }
it { is_expected.to validate_presence_of :sha }
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_email }
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
new file mode 100644
index 00000000000..f70716c9d19
--- /dev/null
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -0,0 +1,81 @@
+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
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fea8182bd30..31b93850c7c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do
target_branch: 'feature',
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
@oldrev = @commits.last.id
@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open }
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
context 'push to origin repo target branch' do
@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@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(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'manual merge of source branch' do
@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_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
context 'push to fork repo source branch' do
@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do
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).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo target branch' do
@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
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
context 'push to origin repo target branch after fork project was removed' do
@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
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
context 'push new branch that exists in a merge request' do
@@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
+ @build_failed_todo.reload
+ @fork_build_failed_todo.reload
end
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a075496ee63..42147736532 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -305,6 +305,25 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
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
def should_create_todo(attributes = {})