Commit d95c0f5f authored by Robert Speicher's avatar Robert Speicher

Merge remote-tracking branch 'ce/master'

parents f9068a84 c4b35a62
...@@ -153,11 +153,10 @@ bundler:audit: ...@@ -153,11 +153,10 @@ bundler:audit:
stage: test stage: test
script: script:
- "bundle exec bundle-audit update" - "bundle exec bundle-audit update"
- "bundle exec bundle-audit check" - "bundle exec bundle-audit check --ignore OSVDB-115941"
tags: tags:
- ruby - ruby
- mysql - mysql
allow_failure: true
# Ruby 2.2 jobs # Ruby 2.2 jobs
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased) v 8.6.0 (unreleased)
- Bump gitlab_git to 9.0.3 (Stan Hu)
- Support Golang subpackage fetching (Stan Hu) - Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu)
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during - Removed the default password from the initial admin account created during
...@@ -18,10 +20,14 @@ v 8.6.0 (unreleased) ...@@ -18,10 +20,14 @@ v 8.6.0 (unreleased)
- Return empty array instead of 404 when commit has no statuses in commit status API - Return empty array instead of 404 when commit has no statuses in commit status API
- Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip) - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
- Rewrite logo to simplify SVG code (Sean Lang) - Rewrite logo to simplify SVG code (Sean Lang)
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
- Ignore jobs that start with `.` (hidden jobs)
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance - Refactor and greatly improve search performance
- Add support for cross-project label references - Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users - Allow search for logged out users
- Allow to define on which builds the current one depends on
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - Don't show Issues/MRs from archived projects in Groups view
- Increase the notes polling timeout over time (Roberto Dip) - Increase the notes polling timeout over time (Roberto Dip)
......
...@@ -283,7 +283,7 @@ group :development, :test do ...@@ -283,7 +283,7 @@ group :development, :test do
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.0.0' gem 'ffaker', '~> 2.0.0'
gem 'capybara', '~> 2.4.0' gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
......
...@@ -108,7 +108,8 @@ GEM ...@@ -108,7 +108,8 @@ GEM
thor (~> 0.18) thor (~> 0.18)
byebug (8.2.1) byebug (8.2.1)
cal-heatmap-rails (3.5.1) cal-heatmap-rails (3.5.1)
capybara (2.4.4) capybara (2.6.2)
addressable
mime-types (>= 1.16) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
...@@ -380,7 +381,7 @@ GEM ...@@ -380,7 +381,7 @@ GEM
gitlab-license (0.0.4) gitlab-license (0.0.4)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.1) gitlab_git (9.0.3)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -928,7 +929,7 @@ DEPENDENCIES ...@@ -928,7 +929,7 @@ DEPENDENCIES
bundler-audit bundler-audit
byebug byebug
cal-heatmap-rails (~> 3.5.0) cal-heatmap-rails (~> 3.5.0)
capybara (~> 2.4.0) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
......
...@@ -111,6 +111,8 @@ window.onload = -> ...@@ -111,6 +111,8 @@ window.onload = ->
setTimeout shiftWindow, 100 setTimeout shiftWindow, 100
$ -> $ ->
bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents # Click a .js-select-on-focus field, select the contents
...@@ -260,35 +262,14 @@ $ -> ...@@ -260,35 +262,14 @@ $ ->
$('.right-sidebar') $('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' }) .hasClass('right-sidebar-collapsed'), { path: '/' })
bootstrapBreakpoint = undefined;
checkBootstrapBreakpoints = ->
if $('.device-xs').is(':visible')
bootstrapBreakpoint = "xs"
else if $('.device-sm').is(':visible')
bootstrapBreakpoint = "sm"
else if $('.device-md').is(':visible')
bootstrapBreakpoint = "md"
else if $('.device-lg').is(':visible')
bootstrapBreakpoint = "lg"
setBootstrapBreakpoints = ->
if $('.device-xs').length
return
$("body")
.append('<div class="device-xs visible-xs"></div>'+
'<div class="device-sm visible-sm"></div>'+
'<div class="device-md visible-md"></div>'+
'<div class="device-lg visible-lg"></div>')
checkBootstrapBreakpoints()
fitSidebarForSize = -> fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint oldBootstrapBreakpoint = bootstrapBreakpoint
checkBootstrapBreakpoints() bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = -> checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm" if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
...@@ -297,6 +278,5 @@ $ -> ...@@ -297,6 +278,5 @@ $ ->
.on "resize", (e) -> .on "resize", (e) ->
fitSidebarForSize() fitSidebarForSize()
setBootstrapBreakpoints()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
class @Breakpoints
instance = null;
class BreakpointInstance
BREAKPOINTS = ["xs", "sm", "md", "lg"]
constructor: ->
@setup()
setup: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
return if $(allDeviceSelector.join(",")).length
# Create all the elements
els = $.map BREAKPOINTS, (breakpoint) ->
"<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
$("body").append els.join('')
getBreakpointSize: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
$visibleDevice = $(allDeviceSelector.join(",")).filter(":visible")
return $visibleDevice.attr("class").split("visible-")[1]
@get: ->
return instance ?= new BreakpointInstance
$ =>
@bp = Breakpoints.get()
...@@ -251,7 +251,7 @@ class GitLabDropdown ...@@ -251,7 +251,7 @@ class GitLabDropdown
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.toggleClass "is-active"
if value if value?
if !field.length if !field.length
# Create hidden input for form # Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />" input = "<input type='hidden' name='#{fieldName}' />"
......
$(document).on("click", '.toggle-nav-collapse', (e) -> collapsed = 'page-sidebar-collapsed'
e.preventDefault() expanded = 'page-sidebar-expanded'
collapsed = 'page-sidebar-collapsed'
expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
...@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> ...@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar(); niceScrollBars.updateScrollBar();
), 300 ), 300
$(document).on("click", '.toggle-nav-collapse', (e) ->
e.preventDefault()
toggleSidebar()
) )
$ ->
size = bp.getBreakpointSize()
if size is "xs" or size is "sm"
if $('.page-with-sidebar').hasClass(expanded)
toggleSidebar()
...@@ -141,22 +141,18 @@ header { ...@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width; margin-left: $sidebar_collapsed_width;
} }
@media (max-width: $screen-md-max) { .header-collapsed {
.header-collapsed {
margin-left: $sidebar_collapsed_width; margin-left: $sidebar_collapsed_width;
}
.header-expanded { @media (min-width: $screen-md-min) {
margin-left: $sidebar_width; @include collapsed-header;
} }
} }
@media(min-width: $screen-md-max) { .header-expanded {
.header-collapsed { margin-left: $sidebar_collapsed_width;
@include collapsed-header;
}
.header-expanded { @media (min-width: $screen-md-min) {
margin-left: $sidebar_width; margin-left: $sidebar_width;
} }
} }
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
} }
.sidebar-wrapper { .sidebar-wrapper {
z-index: 99; z-index: 999;
background: $background-color; background: $background-color;
} }
...@@ -203,7 +203,11 @@ ...@@ -203,7 +203,11 @@
} }
@mixin expanded-sidebar { @mixin expanded-sidebar {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width; padding-left: $sidebar_width;
}
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */ /* Extra small devices (phones, less than 768px) */
......
...@@ -141,7 +141,7 @@ class GroupsController < Groups::ApplicationController ...@@ -141,7 +141,7 @@ class GroupsController < Groups::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :membership_lock, :share_with_group_lock, :public) params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock, :membership_lock)
end end
def load_events def load_events
......
...@@ -37,8 +37,6 @@ ...@@ -37,8 +37,6 @@
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace'] LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
...@@ -128,7 +126,7 @@ module Ci ...@@ -128,7 +126,7 @@ module Ci
end end
def retried? def retried?
!self.commit.latest_builds_for_ref(self.ref).include?(self) !self.commit.latest_statuses_for_ref(self.ref).include?(self)
end end
def depends_on_builds def depends_on_builds
...@@ -309,22 +307,6 @@ module Ci ...@@ -309,22 +307,6 @@ module Ci
project.valid_runners_token? token project.valid_runners_token? token
end end
def target_url
namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
retry_namespace_project_build_path(project.namespace, project, self)
end
end
def can_be_served?(runner) def can_be_served?(runner)
(tag_list - runner.tag_list).empty? (tag_list - runner.tag_list).empty?
end end
...@@ -333,7 +315,7 @@ module Ci ...@@ -333,7 +315,7 @@ module Ci
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
def show_warning? def stuck?
pending? && !any_runners_online? pending? && !any_runners_online?
end end
...@@ -349,18 +331,6 @@ module Ci ...@@ -349,18 +331,6 @@ module Ci
artifacts_file.exists? artifacts_file.exists?
end end
def artifacts_download_url
if artifacts?
download_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browse_url
if artifacts_metadata?
browse_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_metadata? def artifacts_metadata?
artifacts? && artifacts_metadata.exists? artifacts? && artifacts_metadata.exists?
end end
......
...@@ -25,8 +25,6 @@ module Ci ...@@ -25,8 +25,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'
scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
validates_presence_of :sha validates_presence_of :sha
validate :valid_commit_sha validate :valid_commit_sha
...@@ -42,16 +40,6 @@ module Ci ...@@ -42,16 +40,6 @@ module Ci
project.id project.id
end end
def last_build
builds.order(:id).last
end
def retry
latest_builds.each do |build|
Ci::Build.retry(build)
end
end
def valid_commit_sha def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
...@@ -121,12 +109,14 @@ module Ci ...@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a @latest_statuses ||= statuses.latest.to_a
end end
def latest_builds def latest_statuses_for_ref(ref)
@latest_builds ||= builds.latest.to_a latest_statuses.select { |status| status.ref == ref }
end end
def latest_builds_for_ref(ref) def matrix_builds(build = nil)
latest_builds.select { |build| build.ref == ref } matrix_builds = builds.latest.ordered
matrix_builds = matrix_builds.similar(build) if build
matrix_builds.to_a
end end
def retried def retried
...@@ -170,7 +160,7 @@ module Ci ...@@ -170,7 +160,7 @@ module Ci
end end
def duration def duration
duration_array = latest_statuses.map(&:duration).compact duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i duration_array.reduce(:+).to_i
end end
...@@ -183,16 +173,12 @@ module Ci ...@@ -183,16 +173,12 @@ module Ci
end end
def coverage def coverage
coverage_array = latest_builds.map(&:coverage).compact coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1 if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size) '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end end
end end
def matrix_for_ref?(ref)
latest_builds_for_ref(ref).size > 1
end
def config_processor def config_processor
return nil unless ci_yaml_file return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
...@@ -218,10 +204,6 @@ module Ci ...@@ -218,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end end
def update_committed!
update!(committed_at: DateTime.now)
end
private private
def save_yaml_error(error) def save_yaml_error(error)
......
...@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
end end
end end
def cancel_url def stuck?
nil
end
def retry_url
nil
end
def show_warning?
false false
end end
def artifacts_download_url
nil
end
def artifacts_browse_url
nil
end
end end
...@@ -179,26 +179,29 @@ class Note < ActiveRecord::Base ...@@ -179,26 +179,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end end
# Check if such line of code exists in merge request diff # Check if this note is part of an "active" discussion
# If exists - its active discussion #
# If not - its outdated diff # This will always return true for anything except MergeRequest noteables,
# which have special logic.
#
# If the note's current diff cannot be matched in the MergeRequest's current
# diff, it's considered inactive.
def active? def active?
return true unless self.diff return true unless self.diff
return false unless noteable return false unless noteable
return @active if defined?(@active) return @active if defined?(@active)
diffs = noteable.diffs(Commit.max_diff_options) noteable_diff = find_noteable_diff
notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
return @active = false if notable_diff.nil? if noteable_diff
parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
# We cannot use ||= because @active may be false
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
else
@active = false
end end
def outdated? @active
!active?
end end
def diff_file_index def diff_file_index
...@@ -386,6 +389,12 @@ class Note < ActiveRecord::Base ...@@ -386,6 +389,12 @@ class Note < ActiveRecord::Base
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
# Find the diff on noteable that matches our own
def find_noteable_diff
diffs = noteable.diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
def awards_supported? def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line? (for_issue? || for_merge_request?) && !for_diff_line?
end end
......
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
def execute(project, opts) def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref]) sha = opts[:sha] || ref_sha(project, opts[:ref])
commit = project.ci_commits.ordered.find_by(sha: sha) commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit) image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name) image_path = Rails.root.join('public/ci', image_name)
......
...@@ -35,7 +35,6 @@ class CreateCommitBuildsService ...@@ -35,7 +35,6 @@ class CreateCommitBuildsService
unless commit.skip_ci? unless commit.skip_ci?
# Create builds for commit # Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref) tag = Gitlab::Git.tag_ref?(origin_ref)
commit.update_committed!
commit.create_builds(ref, tag, user) commit.create_builds(ref, tag, user)
end end
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
= ci_status_with_icon(build.status) = ci_status_with_icon(build.status)
%td.build-link %td.build-link
- if can?(current_user, :read_build, project) && build.target_url - if can?(current_user, :read_build, build.project)
= link_to build.target_url do = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong Build ##{build.id} %strong Build ##{build.id}
- else - else
%strong Build ##{build.id} %strong Build ##{build.id}
- if build.show_warning? - if build.stuck?
%i.fa.fa-warning.text-warning %i.fa.fa-warning.text-warning
%td %td
...@@ -18,11 +18,11 @@ ...@@ -18,11 +18,11 @@
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
%td %td
= link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace" = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td %td
- if build.ref - if build.ref
= link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref) = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else - else
.light none .light none
...@@ -61,13 +61,12 @@ ...@@ -61,13 +61,12 @@
%td %td
.pull-right .pull-right
- if can?(current_user, :read_build, project) && build.artifacts? - if can?(current_user, :read_build, project) && build.artifacts?
= link_to build.artifacts_download_url, title: 'Download artifacts' do = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do
%i.fa.fa-download %i.fa.fa-download
- if can?(current_user, :update_build, build.project) - if can?(current_user, :update_build, build.project)
- if build.active? - if build.active?
- if build.cancel_url = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do
= link_to build.cancel_url, method: :post, title: 'Cancel' do
%i.fa.fa-remove.cred %i.fa.fa-remove.cred
- elsif defined?(allow_retry) && allow_retry && build.retry_url - elsif defined?(allow_retry) && allow_retry && build.retryable?
= link_to build.retry_url, method: :post, title: 'Retry' do = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do
%i.fa.fa-repeat %i.fa.fa-repeat
%tr.build
%td.status
= ci_status_with_icon(commit.status)
- if commit.running?
&middot;
= commit.stage
%td.build-link
= link_to ci_status_path(commit) do
%strong #{commit.short_sha}
%td.build-message
%span= truncate_first_line(commit.git_commit_message)
%td.build-branch
- unless @ref
%span
- commit.refs.each do |ref|
= link_to truncate(ref, length: 25), ci_project_path(@project, ref: ref)
%td.duration
- if commit.duration > 0
#{time_interval_in_words commit.duration}
%td.timestamp
- if commit.finished_at
%span #{time_ago_in_words commit.finished_at} ago
- if commit.coverage
%td.coverage
#{commit.coverage}%
...@@ -45,6 +45,9 @@ ...@@ -45,6 +45,9 @@
.tab-pane#shared .tab-pane#shared
= render "shared_projects", projects: @shared_projects = render "shared_projects", projects: @shared_projects
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
- else - else
%p.nav-links.no-top %p.nav-links.no-top
No projects to show No projects to show
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('home fw') = icon('bookmark fw')
%span %span
Project Project
= nav_link(path: 'projects#activity') do = nav_link(path: 'projects#activity') do
......
...@@ -55,7 +55,6 @@ ...@@ -55,7 +55,6 @@
%th Coverage %th Coverage
%th %th
- @builds.each do |build| = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled?
= render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true
= paginate @builds, theme: 'gitlab' = paginate @builds, theme: 'gitlab'
...@@ -13,9 +13,10 @@ ...@@ -13,9 +13,10 @@
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace #up-build-trace
- if @commit.matrix_for_ref?(@build.ref) - builds = @build.commit.matrix_builds(@build)
- if builds.size > 1
%ul.nav-links.no-top.no-bottom %ul.nav-links.no-top.no-bottom
- @commit.latest_builds_for_ref(@build.ref).each do |build| - builds.each do |build|
%li{class: ('active' if build == @build) } %li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do = link_to namespace_project_build_path(@project.namespace, @project, build) do
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
...@@ -44,7 +45,7 @@ ...@@ -44,7 +45,7 @@
.pull-right .pull-right
#{time_ago_with_tooltip(@build.finished_at) if @build.finished_at} #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- if @build.show_warning? - if @build.stuck?
- unless @build.any_runners_online? - unless @build.any_runners_online?
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning
%p %p
...@@ -100,12 +101,12 @@ ...@@ -100,12 +101,12 @@
%h4.title Build artifacts %h4.title Build artifacts
.center .center
.btn-group{ role: :group } .btn-group{ role: :group }
= link_to @build.artifacts_download_url, class: 'btn btn-sm btn-primary' do = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('download') = icon('download')
Download Download
- if @build.artifacts_metadata? - if @build.artifacts_metadata?
= link_to @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' do = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do
= icon('folder-open') = icon('folder-open')
Browse Browse
...@@ -115,10 +116,10 @@ ...@@ -115,10 +116,10 @@
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_build, @project)
.center .center
.btn-group{ role: :group } .btn-group{ role: :group }
- if @build.cancel_url - if @build.active?
= link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post
- elsif @build.retry_url - elsif @build.retryable?
= link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
- if @build.erasable? - if @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build), = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
......
%tr.build
%td.status
- if can?(current_user, :read_build, build)
= link_to namespace_project_build_url(build.project.namespace, build.project, build), class: "ci-status ci-#{build.status}" do
= ci_status_with_icon(build.status)
- else
= ci_status_with_icon(build.status)
%td.build-link
- if can?(current_user, :read_build, build)
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%strong ##{build.id}
- else
%strong ##{build.id}
- if build.stuck?
%i.fa.fa-warning.text-warning
- if defined?(commit_sha) && commit_sha
%td
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
%td
- if build.ref
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- else
.light none
- if defined?(runner) && runner
%td
- if build.try(:runner)
= runner_link(build.runner)
- else
.light none
- if defined?(stage) && stage
%td
= build.stage
%td
= build.name
.pull-right
- 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
%td.duration
- if build.duration
#{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp
- if build.finished_at
%span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
- if build.try(:coverage)
#{build.coverage}%
%td
.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' do
%i.fa.fa-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' do
%i.fa.fa-remove.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' do
%i.fa.fa-repeat
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
%th Coverage %th Coverage
%th %th
- @ci_commit.refs.each do |ref| - @ci_commit.refs.each do |ref|
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - builds = @ci_commit.statuses.for_ref(ref).latest.ordered
locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true } = render builds, coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true
- if @ci_commit.retried.any? - if @ci_commit.retried.any?
.gray-content-block.second-block .gray-content-block.second-block
...@@ -64,5 +64,4 @@ ...@@ -64,5 +64,4 @@
- if @ci_commit.project.build_coverage_enabled? - if @ci_commit.project.build_coverage_enabled?
%th Coverage %th Coverage
%th %th
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, = render @ci_commit.retried, coverage: @ci_commit.project.build_coverage_enabled?, stage: true
locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
%tr.commit_status
%td.status
- if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
= link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
= ci_icon_for_status(commit_status.status)
= commit_status.status
- else
= ci_status_with_icon(commit_status.status)
%td.commit_status-link
- if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url
= link_to commit_status.target_url do
%strong ##{commit_status.id}
- else
%strong ##{commit_status.id}
- if commit_status.show_warning?
%i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"}
- if defined?(commit_sha) && commit_sha
%td
= link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
%td
- if commit_status.ref
= link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
- else
.light none
- if defined?(runner) && runner
%td
- if commit_status.try(:runner)
= runner_link(commit_status.runner)
- else
.light none
- if defined?(stage) && stage
%td
= commit_status.stage
%td
= commit_status.name
.pull-right
- if commit_status.tags.any?
- commit_status.tags.each do |tag|
%span.label.label-primary
= tag
- if commit_status.try(:trigger_request)
%span.label.label-info triggered
- if commit_status.try(:allow_failure)
%span.label.label-danger allowed to fail
%td.duration
- if commit_status.duration
#{duration_in_words(commit_status.finished_at, commit_status.started_at)}
%td.timestamp
- if commit_status.finished_at
%span #{time_ago_with_tooltip(commit_status.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
- if commit_status.try(:coverage)
#{commit_status.coverage}%
%td
.pull-right
- if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url
= link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if can?(current_user, :update_commit_status, commit_status)
- if commit_status.active?
- if commit_status.cancel_url
= link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
%i.fa.fa-remove.cred
- elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
= link_to commit_status.retry_url, method: :post, title: 'Retry' do
%i.fa.fa-repeat
%tr.generic_commit_status
%td.status
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url, class: "ci-status ci-#{generic_commit_status.status}" do
= ci_status_with_icon(generic_commit_status.status)
- else
= ci_status_with_icon(generic_commit_status.status)
%td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url do
%strong ##{generic_commit_status.id}
- else
%strong ##{generic_commit_status.id}
- 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"
%td
- if generic_commit_status.ref
= link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
- else
.light none
- if defined?(runner) && runner
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
- else
.light none
- if defined?(stage) && stage
%td
= generic_commit_status.stage
%td
= generic_commit_status.name
.pull-right
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
%span.label.label-primary
= tag
%td.duration
- if generic_commit_status.duration
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
%td.timestamp
- if generic_commit_status.finished_at
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
- if generic_commit_status.try(:coverage)
#{generic_commit_status.coverage}%
%td
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
%a{href: "#", data: {id: "close"}} Closed %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline .filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } }) placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
......
...@@ -33,7 +33,6 @@ Example of response ...@@ -33,7 +33,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2015-12-24T15:51:21.802Z", "created_at": "2015-12-24T15:51:21.802Z",
"download_url": null,
"artifacts_file": { "artifacts_file": {
"filename": "artifacts.zip", "filename": "artifacts.zip",
"size": 1000 "size": 1000
...@@ -75,7 +74,6 @@ Example of response ...@@ -75,7 +74,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2015-12-24T15:51:21.727Z", "created_at": "2015-12-24T15:51:21.727Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z", "finished_at": "2015-12-24T17:54:24.921Z",
"id": 6, "id": 6,
...@@ -139,7 +137,6 @@ Example of response ...@@ -139,7 +137,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2016-01-11T10:13:33.506Z", "created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z", "finished_at": "2016-01-11T10:14:09.526Z",
"id": 69, "id": 69,
...@@ -164,7 +161,6 @@ Example of response ...@@ -164,7 +161,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2015-12-24T15:51:21.957Z", "created_at": "2015-12-24T15:51:21.957Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:33.913Z", "finished_at": "2015-12-24T17:54:33.913Z",
"id": 9, "id": 9,
...@@ -226,7 +222,6 @@ Example of response ...@@ -226,7 +222,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2015-12-24T15:51:21.880Z", "created_at": "2015-12-24T15:51:21.880Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z", "finished_at": "2015-12-24T17:54:31.198Z",
"id": 8, "id": 8,
...@@ -315,7 +310,6 @@ Example of response ...@@ -315,7 +310,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2016-01-11T10:13:33.506Z", "created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z", "finished_at": "2016-01-11T10:14:09.526Z",
"id": 69, "id": 69,
...@@ -362,7 +356,6 @@ Example of response ...@@ -362,7 +356,6 @@ Example of response
}, },
"coverage": null, "coverage": null,
"created_at": "2016-01-11T10:13:33.506Z", "created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"artifacts_file": null, "artifacts_file": null,
"finished_at": null, "finished_at": null,
"id": 69, "id": 69,
......
...@@ -116,7 +116,8 @@ Alias for [stages](#stages). ...@@ -116,7 +116,8 @@ Alias for [stages](#stages).
### variables ### variables
_**Note:** Introduced in GitLab Runner v0.5.0._ >**Note:**
Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
environment. The variables are stored in the git repository and are meant to environment. The variables are stored in the git repository and are meant to
...@@ -153,7 +154,8 @@ cache: ...@@ -153,7 +154,8 @@ cache:
#### cache:key #### cache:key
_**Note:** Introduced in GitLab Runner v1.0.0._ >**Note:**
Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs, between jobs, allowing to have a single cache for all jobs,
...@@ -234,13 +236,14 @@ job_name: ...@@ -234,13 +236,14 @@ job_name:
| Keyword | Required | Description | | Keyword | Required | Description |
|---------------|----------|-------------| |---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner | | script | yes | Defines a shell script which is executed by runner |
| stage | no (default: `test`) | Defines a build stage | | stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` | | type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created | | only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created | | except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select runner | | tags | no | Defines a list of tags which are used to select runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts | | artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs | | cache | no | Define list of files that should be cached between subsequent runs |
...@@ -393,15 +396,18 @@ The above script will: ...@@ -393,15 +396,18 @@ The above script will:
### artifacts ### artifacts
_**Note:** Introduced in GitLab Runner v0.7.0 for non-Windows platforms._ >**Notes:**
>
_**Note:** Limited Windows support was added in GitLab Runner v.1.0.0. > - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
Currently not all executors are supported._ > - Limited Windows support was added in GitLab Runner v.1.0.0.
> - Currently not all executors are supported.
_**Note:** Build artifacts are only collected for successful builds._ > - Build artifacts are only collected for successful builds.
`artifacts` is used to specify list of files and directories which should be `artifacts` is used to specify list of files and directories which should be
attached to build after success. Below are some examples. attached to build after success. To pass artifacts between different builds,
see [dependencies](#dependencies).
Below are some examples.
Send all files in `binaries` and `.config`: Send all files in `binaries` and `.config`:
...@@ -453,9 +459,130 @@ release-job: ...@@ -453,9 +459,130 @@ release-job:
The artifacts will be sent to GitLab after a successful build and will The artifacts will be sent to GitLab after a successful build and will
be available for download in the GitLab UI. be available for download in the GitLab UI.
#### artifacts:name
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts
archive. That way, you can have a unique name of every archive which could be
useful when you'd like to download the archive from GitLab. The `artifacts:name`
variable can make use of any of the [predefined variables](../variables/README.md).
---
**Example configurations**
To create an archive with a name of the current build:
```yaml
job:
artifacts:
name: "$CI_BUILD_NAME"
```
To create an archive with a name of the current branch or tag including only
the files that are untracked by Git:
```yaml
job:
artifacts:
name: "$CI_BUILD_REF_NAME"
untracked: true
```
To create an archive with a name of the current build and the current branch or
tag including only the files that are untracked by Git:
```yaml
job:
artifacts:
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
untracked: true
```
To create an archive with a name of the current [stage](#stages) and branch name:
```yaml
job:
artifacts:
name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
untracked: true
```
---
If you use **Windows Batch** to run your shell scripts you need to replace
`$` with `%`:
```yaml
job:
artifacts:
name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
untracked: true
```
### dependencies
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds.
Note that `artifacts` from previous [stages](#stages) are passed by default.
To use this feature, define `dependencies` in context of the job and pass
a list of all previous builds from which the artifacts should be downloaded.
You can only define builds from stages that are executed before the current one.
An error will be shown if you define builds from the current stage or next ones.
---
In the following example, we define two jobs with artifacts, `build:osx` and
`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
will be downloaded and extracted in the context of the build. The same happens
for `test:linux` and artifacts from `build:linux`.
The job `deploy` will download artifacts from all previous builds because of
the [stage](#stages) precedence:
```yaml
build:osx:
stage: build
script: make build:osx
artifacts:
paths:
- binaries/
build:linux:
stage: build
script: make build:linux
artifacts:
paths:
- binaries/
test:osx:
stage: test
script: make test:osx
dependencies:
- build:osx
test:linux:
stage: test
script: make test:linux
dependencies:
- build:linux
deploy:
stage: deploy
script: make deploy
```
### cache ### cache
_**Note:** Introduced in GitLab Runner v0.7.0._ >**Note:**
Introduced in GitLab Runner v0.7.0.
`cache` is used to specify list of files and directories which should be cached `cache` is used to specify list of files and directories which should be cached
between builds. Below are some examples: between builds. Below are some examples:
...@@ -538,6 +665,155 @@ pages: ...@@ -538,6 +665,155 @@ pages:
Read more on [GitLab Pages user documentation](../../pages/README.md). Read more on [GitLab Pages user documentation](../../pages/README.md).
## Hidden jobs
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
[special YAML features](#special-yaml-features) and transform the hidden jobs
into templates.
In the following example, `.job_name` will be ignored:
```yaml
.job_name:
script:
- rake spec
```
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
and map merging (`<<`), which will allow you to greatly reduce the complexity
of `.gitlab-ci.yml`.
Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
### Anchors
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
`test1` and `test2`, that will inherit the parameters of `.job_template`, each
having their own custom `script` defined:
```yaml
.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
image: ruby:2.1
services:
- postgres
- redis
test1:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test1 project
test2:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test2 project
```
`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
given hash into the current one", and `*` includes the named anchor
(`job_definition` again). The expanded version looks like this:
```yaml
.job_template:
image: ruby:2.1
services:
- postgres
- redis
test1:
image: ruby:2.1
services:
- postgres
- redis
script:
- test1 project
test2:
image: ruby:2.1
services:
- postgres
- redis
script:
- test2 project
```
Let's see another one example. This time we will use anchors to define two sets
of services. This will create two jobs, `test:postgres` and `test:mysql`, that
will share the `script` directive defined in `.job_template`, and the `services`
directive defined in `.postgres_services` and `.mysql_services` respectively:
```yaml
.job_template: &job_definition
script:
- test project
.postgres_services:
services: &postgres_definition
- postgres
- ruby
.mysql_services:
services: &mysql_definition
- mysql
- ruby
test:postgres:
<< *job_definition
services: *postgres_definition
test:mysql:
<< *job_definition
services: *mysql_definition
```
The expanded version looks like this:
```yaml
.job_template:
script:
- test project
.postgres_services:
services:
- postgres
- ruby
.mysql_services:
services:
- mysql
- ruby
test:postgres:
script:
- test project
services:
- postgres
- ruby
test:mysql:
script:
- test project
services:
- mysql
- ruby
```
You can see that the hidden jobs are conveniently used as templates.
## Validate the .gitlab-ci.yml ## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint. Each instance of GitLab CI has an embedded debug tool called Lint.
......
...@@ -13,7 +13,6 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -13,7 +13,6 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
thumbsup = page.first('.award-control') thumbsup = page.first('.award-control')
thumbsup.click thumbsup.click
thumbsup.hover thumbsup.hover
sleep 0.3
end end
end end
...@@ -46,12 +45,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -46,12 +45,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end end
step 'I have award added' do step 'I have award added' do
sleep 0.2
page.within '.awards' do page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn' expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me') expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
end end
end end
......
...@@ -68,7 +68,7 @@ module SharedBuilds ...@@ -68,7 +68,7 @@ module SharedBuilds
end end
step 'I see the build' do step 'I see the build' do
page.within('.commit_status') do page.within('.build') do
expect(page).to have_content "##{@build.id}" expect(page).to have_content "##{@build.id}"
expect(page).to have_content @build.sha[0..7] expect(page).to have_content @build.sha[0..7]
expect(page).to have_content @build.ref expect(page).to have_content @build.ref
......
...@@ -434,13 +434,6 @@ module API ...@@ -434,13 +434,6 @@ module API
expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at expose :created_at, :started_at, :finished_at
expose :user, with: User expose :user, with: User
# TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
# for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
expose :download_url do |repo_obj, options|
if options[:user_can_download_artifacts]
repo_obj.artifacts_download_url
end
end
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? } expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit do |repo_obj, _options| expose :commit, with: RepoCommit do |repo_obj, _options|
if repo_obj.respond_to?(:commit) if repo_obj.respond_to?(:commit)
......
...@@ -5,7 +5,9 @@ module Ci ...@@ -5,7 +5,9 @@ module Ci
DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test' DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache] ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache] ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
:dependencies]
attr_reader :before_script, :image, :services, :variables, :path, :cache attr_reader :before_script, :image, :services, :variables, :path, :cache
...@@ -60,6 +62,7 @@ module Ci ...@@ -60,6 +62,7 @@ module Ci
@jobs = {} @jobs = {}
@config.each do |key, job| @config.each do |key, job|
next if key.to_s.start_with?('.')
stage = job[:stage] || job[:type] || DEFAULT_STAGE stage = job[:stage] || job[:type] || DEFAULT_STAGE
@jobs[key] = { stage: stage }.merge(job) @jobs[key] = { stage: stage }.merge(job)
end end
...@@ -81,6 +84,7 @@ module Ci ...@@ -81,6 +84,7 @@ module Ci
services: job[:services] || @services, services: job[:services] || @services,
artifacts: job[:artifacts], artifacts: job[:artifacts],
cache: job[:cache] || @cache, cache: job[:cache] || @cache,
dependencies: job[:dependencies],
}.compact }.compact
} }
end end
...@@ -143,6 +147,7 @@ module Ci ...@@ -143,6 +147,7 @@ module Ci
validate_job_stage!(name, job) if job[:stage] validate_job_stage!(name, job) if job[:stage]
validate_job_cache!(name, job) if job[:cache] validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts] validate_job_artifacts!(name, job) if job[:artifacts]
validate_job_dependencies!(name, job) if job[:dependencies]
end end
private private
...@@ -216,6 +221,10 @@ module Ci ...@@ -216,6 +221,10 @@ module Ci
end end
def validate_job_artifacts!(name, job) def validate_job_artifacts!(name, job)
if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
end
if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked]) if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean" raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
end end
...@@ -225,6 +234,22 @@ module Ci ...@@ -225,6 +234,22 @@ module Ci
end end
end end
def validate_job_dependencies!(name, job)
if !validate_array_of_strings(job[:dependencies])
raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
end
stage_index = stages.index(job[:stage])
job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
unless stages.index(@jobs[dependency][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
def validate_array_of_strings(values) def validate_array_of_strings(values)
values.is_a?(Array) && values.all? { |value| validate_string(value) } values.is_a?(Array) && values.all? { |value| validate_string(value) }
end end
......
...@@ -397,7 +397,7 @@ module Ci ...@@ -397,7 +397,7 @@ module Ci
services: ["mysql"], services: ["mysql"],
before_script: ["pwd"], before_script: ["pwd"],
rspec: { rspec: {
artifacts: { paths: ["logs/", "binaries/"], untracked: true }, artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
script: "rspec" script: "rspec"
} }
}) })
...@@ -417,6 +417,7 @@ module Ci ...@@ -417,6 +417,7 @@ module Ci
image: "ruby:2.1", image: "ruby:2.1",
services: ["mysql"], services: ["mysql"],
artifacts: { artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"], paths: ["logs/", "binaries/"],
untracked: true untracked: true
} }
...@@ -427,6 +428,73 @@ module Ci ...@@ -427,6 +428,73 @@ module Ci
end end
end end
describe "Dependencies" do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' },
test1: { stage: 'test', script: 'test', dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
}
end
subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
context 'no dependencies' do
let(:dependencies) { }
it { expect { subject }.to_not raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.to_not raise_error }
end
context 'undefined dependency' do
let(:dependencies) { [:undefined] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
let(:dependencies) { [:deploy] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
end
describe "Hidden jobs" do
let(:config) do
YAML.dump({
'.hidden_job' => { script: 'test' },
'normal_job' => { script: 'test' }
})
end
let(:config_processor) { GitlabCiYamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("test", "master") }
it "doesn't create jobs that starts with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
except: nil,
stage: "test",
stage_idx: 1,
name: :normal_job,
only: nil,
commands: "\ntest",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false
})
end
end
describe "YAML Alias/Anchor" do describe "YAML Alias/Anchor" do
it "is correctly supported for jobs" do it "is correctly supported for jobs" do
config = <<EOT config = <<EOT
...@@ -629,6 +697,13 @@ EOT ...@@ -629,6 +697,13 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
end
it "returns errors if job artifacts:untracked is not an array of strings" do it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do expect do
...@@ -684,6 +759,13 @@ EOT ...@@ -684,6 +759,13 @@ EOT
GitlabCiYamlProcessor.new(config) GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
end end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
end
end end
end end
end end
...@@ -9,7 +9,7 @@ describe Ci::Build, models: true do ...@@ -9,7 +9,7 @@ describe Ci::Build, models: true do
it { is_expected.to respond_to :trace_html } it { is_expected.to respond_to :trace_html }
describe :first_pending do describe '#first_pending' do
let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday } let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' } let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
before { first; second } before { first; second }
...@@ -19,7 +19,7 @@ describe Ci::Build, models: true do ...@@ -19,7 +19,7 @@ describe Ci::Build, models: true do
it('returns with the first pending build') { is_expected.to eq(first) } it('returns with the first pending build') { is_expected.to eq(first) }
end end
describe :create_from do describe '#create_from' do
before do before do
build.status = 'success' build.status = 'success'
build.save build.save
...@@ -33,7 +33,7 @@ describe Ci::Build, models: true do ...@@ -33,7 +33,7 @@ describe Ci::Build, models: true do
end end
end end
describe :ignored? do describe '#ignored?' do
subject { build.ignored? } subject { build.ignored? }
context 'if build is not allowed to fail' do context 'if build is not allowed to fail' do
...@@ -69,7 +69,7 @@ describe Ci::Build, models: true do ...@@ -69,7 +69,7 @@ describe Ci::Build, models: true do
end end
end end
describe :trace do describe '#trace' do
subject { build.trace_html } subject { build.trace_html }
it { is_expected.to be_empty } it { is_expected.to be_empty }
...@@ -101,7 +101,7 @@ describe Ci::Build, models: true do ...@@ -101,7 +101,7 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(commit.project.timeout) } # it { is_expected.to eq(commit.project.timeout) }
# end # end
describe :options do describe '#options' do
let(:options) do let(:options) do
{ {
image: "ruby:2.1", image: "ruby:2.1",
...@@ -122,25 +122,25 @@ describe Ci::Build, models: true do ...@@ -122,25 +122,25 @@ describe Ci::Build, models: true do
# it { is_expected.to eq(project.allow_git_fetch) } # it { is_expected.to eq(project.allow_git_fetch) }
# end # end
describe :project do describe '#project' do
subject { build.project } subject { build.project }
it { is_expected.to eq(commit.project) } it { is_expected.to eq(commit.project) }
end end
describe :project_id do describe '#project_id' do
subject { build.project_id } subject { build.project_id }
it { is_expected.to eq(commit.project_id) } it { is_expected.to eq(commit.project_id) }
end end
describe :project_name do describe '#project_name' do
subject { build.project_name } subject { build.project_name }
it { is_expected.to eq(project.name) } it { is_expected.to eq(project.name) }
end end
describe :extract_coverage do describe '#extract_coverage' do
context 'valid content & regex' do context 'valid content & regex' do
subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
...@@ -172,7 +172,7 @@ describe Ci::Build, models: true do ...@@ -172,7 +172,7 @@ describe Ci::Build, models: true do
end end
end end
describe :variables do describe '#variables' do
context 'returns variables' do context 'returns variables' do
subject { build.variables } subject { build.variables }
...@@ -242,7 +242,7 @@ describe Ci::Build, models: true do ...@@ -242,7 +242,7 @@ describe Ci::Build, models: true do
end end
end end
describe :can_be_served? do describe '#can_be_served?' do
let(:runner) { FactoryGirl.create :ci_runner } let(:runner) { FactoryGirl.create :ci_runner }
before { build.project.runners << runner } before { build.project.runners << runner }
...@@ -277,7 +277,7 @@ describe Ci::Build, models: true do ...@@ -277,7 +277,7 @@ describe Ci::Build, models: true do
end end
end end
describe :any_runners_online? do describe '#any_runners_online?' do
subject { build.any_runners_online? } subject { build.any_runners_online? }
context 'when no runners' do context 'when no runners' do
...@@ -312,8 +312,8 @@ describe Ci::Build, models: true do ...@@ -312,8 +312,8 @@ describe Ci::Build, models: true do
end end
end end
describe :show_warning? do describe '#stuck?' do
subject { build.show_warning? } subject { build.stuck? }
%w(pending).each do |state| %w(pending).each do |state|
context "if commit_status.status is #{state}" do context "if commit_status.status is #{state}" do
...@@ -343,35 +343,7 @@ describe Ci::Build, models: true do ...@@ -343,35 +343,7 @@ describe Ci::Build, models: true do
end end
end end
describe :artifacts_download_url do describe '#artifacts?' do
subject { build.artifacts_download_url }
context 'artifacts file does not exist' do
before { build.update_attributes(artifacts_file: nil) }
it { is_expected.to be_nil }
end
context 'artifacts file exists' do
let(:build) { create(:ci_build, :artifacts) }
it { is_expected.to_not be_nil }
end
end
describe :artifacts_browse_url do
subject { build.artifacts_browse_url }
it "should be nil if artifacts browser is unsupported" do
allow(build).to receive(:artifacts_metadata?).and_return(false)
is_expected.to be_nil
end
it 'should not be nil if artifacts browser is supported' do
allow(build).to receive(:artifacts_metadata?).and_return(true)
is_expected.to_not be_nil
end
end
describe :artifacts? do
subject { build.artifacts? } subject { build.artifacts? }
context 'artifacts archive does not exist' do context 'artifacts archive does not exist' do
...@@ -386,7 +358,7 @@ describe Ci::Build, models: true do ...@@ -386,7 +358,7 @@ describe Ci::Build, models: true do
end end
describe :artifacts_metadata? do describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? } subject { build.artifacts_metadata? }
context 'artifacts metadata does not exist' do context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
...@@ -398,7 +370,7 @@ describe Ci::Build, models: true do ...@@ -398,7 +370,7 @@ describe Ci::Build, models: true do
end end
end end
describe :repo_url do describe '#repo_url' do
let(:build) { FactoryGirl.create :ci_build } let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project } let(:project) { build.project }
...@@ -412,7 +384,7 @@ describe Ci::Build, models: true do ...@@ -412,7 +384,7 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) } it { is_expected.to include(project.web_url[7..-1]) }
end end
describe :depends_on_builds do describe '#depends_on_builds' do
let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' } let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' }
let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' } let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' }
let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' } let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' }
...@@ -444,7 +416,7 @@ describe Ci::Build, models: true do ...@@ -444,7 +416,7 @@ describe Ci::Build, models: true do
created_at: created_at) created_at: created_at)
end end
describe :merge_request do describe '#merge_request' do
context 'when a MR has a reference to the commit' do context 'when a MR has a reference to the commit' do
before do before do
@merge_request = create_mr(build, commit, factory: :merge_request) @merge_request = create_mr(build, commit, factory: :merge_request)
......
...@@ -32,50 +32,6 @@ describe Ci::Commit, models: true do ...@@ -32,50 +32,6 @@ describe Ci::Commit, models: true do
it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha } it { is_expected.to respond_to :short_sha }
describe :ordered do
let(:project) { FactoryGirl.create :empty_project }
it 'returns ordered list of commits' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
expect(project.ci_commits.ordered).to eq([commit2, commit1])
end
it 'returns commits ordered by committed_at and id, with nulls last' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
end
end
describe :last_build do
subject { commit.last_build }
before do
@first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
@second = FactoryGirl.create :ci_build, commit: commit
end
it { is_expected.to be_a(Ci::Build) }
it('returns with the most recently created build') { is_expected.to eq(@second) }
end
describe :retry do
before do
@first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
@second = FactoryGirl.create :ci_build, commit: commit
end
it "creates only a new build" do
expect(commit.builds.count(:all)).to eq 2
expect(commit.statuses.count(:all)).to eq 2
commit.retry
expect(commit.builds.count(:all)).to eq 3
expect(commit.statuses.count(:all)).to eq 3
end
end
describe :valid_commit_sha do describe :valid_commit_sha do
context 'commit.sha can not start with 00000000' do context 'commit.sha can not start with 00000000' do
before do before do
......
...@@ -152,7 +152,7 @@ describe Note, models: true do ...@@ -152,7 +152,7 @@ describe Note, models: true do
end end
end end
describe :grouped_awards do describe '.grouped_awards' do
before do before do
create :note, note: "smile", is_award: true create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true create :note, note: "smile", is_award: true
...@@ -169,6 +169,66 @@ describe Note, models: true do ...@@ -169,6 +169,66 @@ describe Note, models: true do
end end
end end
describe '#active?' do
it 'is always true when the note has no associated diff' do
note = build(:note)
expect(note).to receive(:diff).and_return(nil)
expect(note).to be_active
end
it 'is never true when the note has no noteable associated' do
note = build(:note)
expect(note).to receive(:diff).and_return(double)
expect(note).to receive(:noteable).and_return(nil)
expect(note).not_to be_active
end
it 'returns the memoized value if defined' do
note = build(:note)
expect(note).to receive(:diff).and_return(double)
expect(note).to receive(:noteable).and_return(double)
note.instance_variable_set(:@active, 'foo')
expect(note).not_to receive(:find_noteable_diff)
expect(note.active?).to eq 'foo'
end
context 'for a merge request noteable' do
it 'is false when noteable has no matching diff' do
merge = build_stubbed(:merge_request, :simple)
note = build(:note, noteable: merge)
allow(note).to receive(:diff).and_return(double)
expect(note).to receive(:find_noteable_diff).and_return(nil)
expect(note).not_to be_active
end
it 'is true when noteable has a matching diff' do
merge = create(:merge_request, :simple)
# Generate a real line_code value so we know it will match. We use a
# random line from a random diff just for funsies.
diff = merge.diffs.to_a.sample
line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
# We're persisting in order to trigger the set_diff callback
note = create(:note, noteable: merge, line_code: code)
# Make sure we don't get a false positive from a guard clause
expect(note).to receive(:find_noteable_diff).and_call_original
expect(note).to be_active
end
end
end
describe "editable?" do describe "editable?" do
it "returns true" do it "returns true" do
note = build(:note) note = build(:note)
......
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