Commit deae7c98 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream

parents 942b11ab 1051f0c5
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
......@@ -12,6 +13,7 @@ v 8.9.0 (unreleased)
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Redesign all Devise emails. !4297
- Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
......@@ -28,6 +30,7 @@ v 8.9.0 (unreleased)
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages
- Fix images in sign-up confirmation email
- Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects
- Fix horizontal scrollbar for long commit message.
......@@ -38,7 +41,9 @@ v 8.9.0 (unreleased)
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
- Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2
- Adds selected branch name to the dropdown toggle
- Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
......@@ -63,7 +68,9 @@ v 8.9.0 (unreleased)
- Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
- Added Gfm autocomplete for labels
- Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
......@@ -107,6 +114,8 @@ v 8.9.0 (unreleased)
- Include user relationships when retrieving award_emoji
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
- Set inverse_of for Project/Service association to reduce the number of queries
- Update tanuki logo highlight/loading colors
- Use Git cached counters for branches and tags on project page
v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166
......@@ -118,6 +127,8 @@ v 8.8.5
- Prevent unauthorized access for projects build traces
- Forbid scripting for wiki files
- Only show notes through JSON on confidential issues that the user has access to
- Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
- Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
......
......@@ -56,7 +56,7 @@ gem "browser", '~> 2.0.3'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 10.0'
gem "gitlab_git", '~> 10.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......
......@@ -299,7 +299,7 @@ GEM
gitlab-license (0.0.4)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.3)
gitlab_git (10.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -905,7 +905,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 0.0.4)
gitlab_emoji (~> 0.3.0)
gitlab_git (~> 10.0)
gitlab_git (~> 10.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
......
class @BlobGitignoreSelector
constructor: (opts) ->
{
@dropdown
@editor
@$wrapper = @dropdown.closest('.gitignore-selector')
@$filenameInput = $('#file_name')
@data = @dropdown.data('filenames')
} = opts
#= require blob/template_selector
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (gitignore) ->
gitignore.name
)
@toggleGitignoreSelector()
@bindEvents()
bindEvents: ->
@$filenameInput
.on 'keyup blur', (e) =>
@toggleGitignoreSelector()
toggleGitignoreSelector: ->
filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
@$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
onClick: (item, el, e) =>
e.preventDefault()
@requestIgnoreFile(item.name)
requestIgnoreFile: (name) ->
Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
requestIgnoreFileSuccess: (gitignore) ->
@editor.setValue(gitignore.content, 1)
@editor.focus()
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
dropdown: $dropdown,
editor: @editor
)
class @BlobGitignoreSelector extends TemplateSelector
requestFile: (query) ->
Api.gitignoreText query.name, @requestFileSuccess.bind(@)
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
class @BlobLicenseSelector
licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
#= require blob/template_selector
constructor: (editor) ->
@$licenseSelector = $('.js-license-selector')
$fileNameInput = $('#file_name')
class @BlobLicenseSelector extends TemplateSelector
requestFile: (query) ->
data =
project: @dropdown.data('project')
fullname: @dropdown.data('fullname')
initialFileNameValue = if $fileNameInput.length
$fileNameInput.val()
else if $('.editor-file-name').length
$('.editor-file-name').text().trim()
@toggleLicenseSelector(initialFileNameValue)
if $fileNameInput
$fileNameInput.on 'keyup blur', (e) =>
@toggleLicenseSelector($(e.target).val())
$('select.license-select').on 'change', (e) ->
data =
project: $(this).data('project')
fullname: $(this).data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
toggleLicenseSelector: (fileName) =>
if @licenseRegex.test(fileName)
@$licenseSelector.show()
else
@$licenseSelector.hide()
Api.licenseText query.id, data, @requestFileSuccess.bind(@)
class @BlobLicenseSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-license-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobLicenseSelector(
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
......@@ -12,8 +12,9 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
new BlobLicenseSelector(@editor)
new BlobGitignoreSelectors(editor: @editor)
new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
......
class @TemplateSelector
constructor: (opts = {}) ->
{
@dropdown,
@data,
@pattern,
@wrapper,
@editor,
@fileEndpoint,
@$input = $('#file_name')
} = opts
@buildDropdown()
@bindEvents()
@onFilenameUpdate()
buildDropdown: ->
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (item) ->
item.name
)
bindEvents: ->
@$input.on('keyup blur', (e) =>
@onFilenameUpdate()
)
onFilenameUpdate: ->
return unless @$input.length
filenameMatches = @pattern.test(@$input.val().trim())
if not filenameMatches
@wrapper.addClass('hidden')
return
@wrapper.removeClass('hidden')
onClick: (item, el, e) =>
e.preventDefault()
@requestFile(item)
requestFile: (item) ->
# To be implemented on the extending class
# e.g.
# Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess: (file) ->
@editor.setValue(file.content, 1)
@editor.focus()
......@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members:
template: '<li>${username} <small>${title}</small></li>'
Labels:
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
# Issues and MergeRequests
Issues:
template: '<li><small>${id}</small> ${title}</li>'
......@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
@input.atwho
at: '~'
alias: 'labels'
searchKey: 'search'
displayTpl: @Labels.template
insertTpl: '${atwho-at}${title}'
callbacks:
beforeSave: (merges) ->
sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title)
"\"#{sanitize(title)}\""
else
sanitize(title)
$.map merges, (m) ->
title: sanitizeLabelTitle(m.title)
color: m.color
search: "#{m.title}"
destroyAtWho: ->
@input.atwho('destroy')
......@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
# load labels
@input.atwho 'load', '~', data.labels
# This trigger at.js again
# otherwise we would be stuck with loading until the user types
......
......@@ -9,7 +9,7 @@ class @MergeRequest
# Options:
# action - String, current controller action
#
constructor: (@opts) ->
constructor: (@opts = {}) ->
this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', =>
......
......@@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight()
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
......
......@@ -91,6 +91,10 @@
background-color: $white-light;
border-top: none;
}
&.top-block .container-fluid {
background-color: inherit;
}
}
.cover-block {
......
......@@ -2,6 +2,17 @@
* Application Header
*
*/
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
&:hover,
&.highlight {
fill: lighten($path-color, 25%);
transition: all 0.1s;
}
}
header {
transition: padding $sidebar-transition-duration;
......@@ -191,13 +202,24 @@ header {
}
}
.tanuki-shape {
transition: all 0.8s;
#tanuki-logo {
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
#tanuki-left-ear,
#tanuki-right-ear,
#tanuki-nose {
@include tanuki-logo-colors($tanuki-red);
}
#tanuki-left-eye,
#tanuki-right-eye {
@include tanuki-logo-colors($tanuki-orange);
}
#tanuki-left-cheek,
#tanuki-right-cheek {
@include tanuki-logo-colors($tanuki-yellow);
}
}
@media (max-width: $screen-xs-max) {
......
......@@ -55,17 +55,6 @@
}
}
.tanuki-shape {
transition: all 0.8s;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
}
.nav-sidebar {
position: absolute;
top: 50px;
......
......@@ -156,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
$tanuki-yellow: #fca326;
/*
* State colors:
*/
......
......@@ -38,6 +38,10 @@ table {
margin: 0 auto;
text-align: left;
width: 600px;
& > td {
text-align: center;
}
}
&#body {
......
......@@ -66,8 +66,7 @@
font-family: $regular_font;
}
.gitignore-selector {
.gitignore-selector, .license-selector {
.dropdown {
line-height: 21px;
}
......
......@@ -244,6 +244,10 @@
.panel-footer {
padding: 5px 10px;
.btn {
min-width: auto;
}
}
.commit {
......@@ -252,9 +256,7 @@
}
.avatar {
width: 20px;
height: 20px;
margin-right: 5px;
margin-left: 0;
}
.commit-row-info {
......
......@@ -28,7 +28,7 @@
.container-fluid {
position: relative;
@media (min-width: $screen-md-max) {
@media (min-width: $screen-lg-min) {
.row {
display: flex;
-ms-flex-align: center;
......@@ -224,7 +224,7 @@
right: 16px;
bottom: 0;
@media (max-width: $screen-lg-min) {
@media (max-width: $screen-md-max) {
top: 0;
}
......@@ -233,7 +233,7 @@
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
@media (max-width: $screen-md-max) {
position: relative;
bottom: 0;
margin-right: 10px;
......
......@@ -144,6 +144,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants
}
......
......@@ -180,8 +180,8 @@ module BlobHelper
licenses = Licensee::License.all
@licenses_for_select = {
Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
end
......
......@@ -6,12 +6,6 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym
end
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || source.members.exists?(user_id: user.id)
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
......
......@@ -94,10 +94,13 @@ module Ci
end
def create_builds(user, trigger_request = nil)
##
# We persist pipeline only if there are builds available
#
return unless config_processor
config_processor.stages.any? do |stage|
CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
end
build_builds_for_stages(config_processor.stages, user,
'success', trigger_request) && save
end
def create_next_builds(build)
......@@ -115,10 +118,10 @@ module Ci
prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status
# create builds for next stages based
next_stages.any? do |stage|
CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
end
# build builds for next stage that has builds available
# and save pipeline if we have builds
build_builds_for_stages(next_stages, build.user, prior_status,
build.trigger_request) && save
end
def retried
......@@ -139,10 +142,10 @@ module Ci
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message)
self.yaml_errors = e.message
nil
rescue
save_yaml_error("Undefined error")
self.yaml_errors = 'Undefined error'
nil
end
end
......@@ -169,6 +172,17 @@ module Ci
private
def build_builds_for_stages(stages, user, status, trigger_request)
##
# Note that `Array#any?` implements a short circuit evaluation, so we
# build builds only for the first stage that has builds available.
#
stages.any? do |stage|
CreateBuildsService.new(self)
.execute(stage, user, status, trigger_request).present?
end
end
def update_state
statuses.reload
self.status = if yaml_errors.blank?
......@@ -181,11 +195,5 @@ module Ci
self.duration = statuses.latest.duration
save
end
def save_yaml_error(error)
return if self.yaml_errors?
self.yaml_errors = error
update_state
end
end
end
......@@ -9,6 +9,12 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy
......
class JiraIssue < ExternalIssue
end
......@@ -294,7 +294,23 @@ class Project < ActiveRecord::Base
#
# Returns a Project, or nil if no project could be found.
def find_with_namespace(path)
where_paths_in([path]).reorder(nil).take
namespace_path, project_path = path.split('/', 2)
return unless namespace_path && project_path
namespace_path = connection.quote(namespace_path)
project_path = connection.quote(project_path)
# On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
# any literal matches come first, for this we have to use "BINARY".
# Without this there's still no guarantee in what order MySQL will return
# rows.
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
"AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple projects by their full paths.
......
......@@ -295,7 +295,7 @@ class Repository
end
def cache_keys
%i(size branch_names tag_names commit_count
%i(size branch_names tag_names branch_count tag_count commit_count
readme version contribution_guide changelog
license_blob license_key gitignore)
end
......
......@@ -2,10 +2,11 @@ module Ci
class CreateBuildsService
def initialize(pipeline)
@pipeline = pipeline
@config = pipeline.config_processor
end
def execute(stage, user, status, trigger_request = nil)
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
......@@ -19,34 +20,37 @@ module Ci
end
end
# don't create the same build twice
builds_attrs.reject! do |build_attrs|
@pipeline.builds.find_by(ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: trigger_request,
name: build_attrs[:name])
end
builds_attrs.map do |build_attrs|
# don't create the same build twice
unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag,
trigger_request: trigger_request, name: build_attrs[:name])
build_attrs.slice!(:name,
:commands,
:tag_list,
:options,
:allow_failure,
:stage,
:stage_idx,
:environment)
build_attrs.slice!(:name,
:commands,
:tag_list,
:options,
:allow_failure,
:stage,
:stage_idx,
:environment)
build_attrs.merge!(ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: trigger_request,
user: user,
project: @pipeline.project)
build_attrs.merge!(pipeline: @pipeline,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: trigger_request,
user: user,
project: @pipeline.project)
@pipeline.builds.create!(build_attrs)
end
##
# We do not persist new builds here.
# Those will be persisted when @pipeline is saved.
#
@pipeline.builds.new(build_attrs)
end
end
private
def config_processor
@config_processor ||= @pipeline.config_processor
end
end
end
......@@ -8,7 +8,9 @@ module Ci
return pipeline
end
unless commit
if commit
pipeline.sha = commit.id
else
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
......@@ -18,22 +20,18 @@ module Ci
return pipeline
end
begin
Ci::Pipeline.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
return pipeline
end
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback
end
pipeline.save!
pipeline.save!
pipeline.create_builds(current_user)
end
rescue
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
unless pipeline.create_builds(current_user)
pipeline.errors.add(:base, 'No builds for this pipeline.')
end
pipeline.save
pipeline
end
......
class CreateCommitBuildsService
def execute(project, user, params, mirror_update: false)
return false unless project.builds_enabled?
return unless project.builds_enabled?
return false if !project.mirror_trigger_builds? && mirror_update
......@@ -9,10 +9,6 @@ class CreateCommitBuildsService
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
unless origin_ref && sha.present?
return false
end
ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref)
......@@ -21,23 +17,50 @@ class CreateCommitBuildsService
return false
end
pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
@pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating pipeline when no gitlab-ci.yml is found
unless pipeline.ci_yaml_file
##
# Skip creating pipeline if no gitlab-ci.yml is found
#
unless @pipeline.ci_yaml_file
return false
end
# Create a new pipeline
pipeline.save!
##
# Skip creating builds for commits that have [ci skip]
unless pipeline.skip_ci?
# Create builds for commit
pipeline.create_builds(user)
# but save pipeline object
#
if @pipeline.skip_ci?
return save_pipeline!
end
##
# Skip creating builds when CI config is invalid
# but save pipeline object
#
unless @pipeline.config_processor
return save_pipeline!
end
pipeline.touch
pipeline
##
# Skip creating pipeline object if there are no builds for it.
#
unless @pipeline.create_builds(user)
@pipeline.errors.add(:base, 'No builds created')
return false
end
save_pipeline!
end
private
##
# Create a new pipeline and touch object to calculate status
#
def save_pipeline!
@pipeline.save!
@pipeline.touch
@pipeline
end
end
......@@ -11,5 +11,9 @@ module Projects
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
def labels
@project.labels.select([:title, :color])
end
end
end
.center
#content
%h2 Hello, #{@resource.name}!
%p
The password for your GitLab account on
#{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}
has successfully been changed.
%p
If you did not initiate this change, please contact your administrator
immediately.
Hello, <%= @resource.name %>!
The password for your GitLab account on <%= Gitlab.config.gitlab.url %>
has successfully been changed.
If you did not initiate this change, please contact your administrator
immediately.
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
.center
#content
%h2 Hello, #{@resource.name}!
%p
Someone, hopefully you, has requested to reset the password for your
GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
%p
If you did not perform this request, you can safely ignore this email.
%p
Otherwise, click the link below to complete the process.
#cta
= link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
Hello, <%= @resource.name %>!
Someone, hopefully you, has requested to reset the password for your GitLab
account on <%= Gitlab.config.gitlab.url %>
If you did not perform this request, you can safely ignore this email.
Otherwise, click the link below to complete the process:
<%= edit_password_url(@resource, reset_password_token: @token) %>
%p
Hello #{@resource.name}!
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in
= time_ago_in_words(Devise.unlock_in.from_now)
or you may click the link below to unlock now.
%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token)
.center
#content
%h2 Hello, #{@resource.name}!
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
or you may click the link below to unlock now.
#cta
= link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
Hello, <%= @resource.name %>!
Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
or you may click the link below to unlock now.
<%= unlock_url(@resource, unlock_token: @token) %>
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}');
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- if event = last_push_event
- if show_last_push_widget?(event)
.row-content-block.top-block.clear-block.hidden-xs
.event-last-push
.event-last-push-text
%span You pushed to
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
%strong= event.ref_name
branch
#{time_ago_with_tooltip(event.created_at)}
%div{ class: (container_class) }
.event-last-push
.event-last-push-text
%span You pushed to
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
%strong= event.ref_name
branch
#{time_ago_with_tooltip(event.created_at)}
.pull-right
= link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
Create Merge Request
.pull-right
= link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
Create Merge Request
......@@ -13,12 +13,10 @@
required: true, class: 'form-control new-file-name'
.pull-right
.license-selector.js-license-selector.hide
= select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
.gitignore-selector.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } )
.license-selector.js-license-selector-wrap.hidden
= dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
......
......@@ -48,16 +48,16 @@
- if @build.active?
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
#js-build-scroll.scroll-controls
= link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
= link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
......
......@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
= dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
......
......@@ -17,11 +17,11 @@
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
%span.dropdown
%span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
%span.caret
%ul.dropdown-menu
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
......@@ -37,7 +37,7 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default
.light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
......
......@@ -23,10 +23,10 @@
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)})
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
#{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)})
#{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
%li
......
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
%span.str-truncated
= link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
- file_name = blob_item.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
%span.str-truncated= file_name
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
%span.str-truncated
- path = flatten_tree(tree_item)
= link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
- path = flatten_tree(tree_item)
= link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
%span.str-truncated= path
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
......@@ -37,7 +37,7 @@
method: :post,
class: 'btn-xs btn'
- if show_roles && can_see_member_roles?(source: member.source, user: current_user)
- if show_roles
%span.pull-right
%strong= member.human_access
- if show_controls
......
......@@ -9,3 +9,4 @@ unless Gitlab.config.gitlab_on_standard_port?
end
Rails.application.routes.default_url_options = default_url_options
ActionMailer::Base.asset_host = Settings.gitlab['base_url']
......@@ -103,7 +103,7 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
each_node do |node|
nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
......@@ -206,6 +206,55 @@ module Banzai
text
end
# Returns a Hash containing all object references (e.g. issue IDs) per the
# project they belong to.
def references_per_project
@references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = Regexp.union(object_class.reference_pattern,
object_class.link_reference_pattern)
nodes.each do |node|
node.to_html.scan(regex) do
project = $~[:project] || current_project_path
refs[project] << $~[object_sym]
end
end
refs
end
end
# Returns a Hash containing referenced projects grouped per their full
# path.
def projects_per_reference
@projects_per_reference ||= begin
hash = {}
refs = Set.new
references_per_project.each do |project_ref, _|
refs << project_ref
end
find_projects_for_paths(refs.to_a).each do |project|
hash[project.path_with_namespace] = project
end
hash
end
end
# Returns the projects for the given paths.
def find_projects_for_paths(paths)
Project.where_paths_in(paths).includes(:namespace)
end
def current_project_path
@current_project_path ||= project.path_with_namespace
end
private
def project_refs_cache
......
......@@ -3,17 +3,8 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
doc.search('a').each do |node|
link = node.attr('href')
next unless link
# Skip non-HTTP(S) links
next unless link.start_with?('http')
# Skip internal links
next if link.start_with?(internal_url)
# Skip non-HTTP(S) links and internal links
doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
......
......@@ -11,13 +11,40 @@ module Banzai
Issue
end
def find_object(project, id)
project.get_issue(id)
def find_object(project, iid)
issues_per_project[project][iid]
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
end
def project_from_ref(ref)
projects_per_reference[ref || current_project_path]
end
# Returns a Hash containing the issues per Project instance.
def issues_per_project
@issues_per_project ||= begin
hash = Hash.new { |h, k| h[k] = {} }
projects_per_reference.each do |path, project|
issue_ids = references_per_project[path]
next unless project.default_issues_tracker?
project.issues.where(iid: issue_ids.to_a).each do |issue|
hash[project][issue.iid] = issue
end
end
hash
end
end
def find_projects_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
end
end
end
......@@ -10,11 +10,11 @@ module Banzai
def call
return doc unless project
doc.search('a').each do |el|
doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
process_link_attr el.attribute('href')
end
doc.search('img').each do |el|
doc.xpath('descendant-or-self::img[starts-with(@src, "/uploads/")]').each do |el|
process_link_attr el.attribute('src')
end
......@@ -24,12 +24,7 @@ module Banzai
protected
def process_link_attr(html_attr)
return if html_attr.blank?
uri = html_attr.value
if uri.starts_with?("/uploads/")
html_attr.value = build_url(uri).to_s
end
html_attr.value = build_url(html_attr.value).to_s
end
def build_url(uri)
......
......@@ -30,7 +30,10 @@ module Ci
end
def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
builds.select do |build|
build[:stage] == stage &&
process?(build[:only], build[:except], ref, tag, trigger_request)
end
end
def builds
......
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
include Select2Helper
include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:project) }
......@@ -21,7 +21,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_selector('.license-selector')
select2('mit', from: '#license_type')
select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
......@@ -44,7 +44,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
select2('mit', from: '#license_type')
select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
......@@ -58,4 +58,12 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Choose a License template'
click_link template
wait_for_ajax
end
end
end
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
include Select2Helper
include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
......@@ -20,7 +20,7 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
select2('mit', from: '#license_type')
select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
......@@ -36,4 +36,12 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Choose a License template'
click_link template
wait_for_ajax
end
end
end
......@@ -77,6 +77,7 @@ feature 'Prioritize labels', feature: true do
end
visit current_url
wait_for_ajax
page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix')
......
......@@ -49,6 +49,13 @@ describe NotesFinder do
user = create(:user)
expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'raises an error for project members with guest role' do
user = create(:user)
project.team << [user, :guest]
expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
......@@ -9,22 +9,6 @@ describe MembersHelper do
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
describe '#can_see_member_roles?' do
let(:project) { create(:empty_project) }
let(:group) { create(:group) }
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
let(:project_member) { create(:project_member, project: project) }
let(:group_member) { create(:group_member, group: group) }
it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy }
it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy }
it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy }
it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy }
it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy }
it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy }
end
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
......
......@@ -33,9 +33,9 @@ describe MergeRequestsHelper do
let(:project) { create(:project) }
let(:issues) do
[
JiraIssue.new('JIRA-123', project),
JiraIssue.new('JIRA-456', project),
JiraIssue.new('FOOBAR-7890', project)
ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('JIRA-456', project),
ExternalIssue.new('FOOBAR-7890', project)
]
end
......
......@@ -6,7 +6,7 @@ describe 'MergeRequest', ->
beforeEach ->
fixture.load('merge_requests_show.html')
@merge = new MergeRequest({})
@merge = new MergeRequest()
it 'modifies the Markdown field', ->
spyOn(jQuery, 'ajax').and.stub()
......
require 'spec_helper'
describe Banzai::Filter::AbstractReferenceFilter do
let(:project) { create(:empty_project) }
describe '#references_per_project' do
it 'returns a Hash containing references grouped per project paths' do
doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
filter = described_class.new(doc, project: project)
expect(filter).to receive(:object_class).twice.and_return(Issue)
expect(filter).to receive(:object_sym).twice.and_return(:issue)
refs = filter.references_per_project
expect(refs).to be_an_instance_of(Hash)
expect(refs[project.to_reference]).to eq(Set.new(%w[1 2]))
end
end
describe '#projects_per_reference' do
it 'returns a Hash containing projects grouped per project paths' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(filter).to receive(:references_per_project).
and_return({ project.path_with_namespace => Set.new(%w[1]) })
expect(filter.projects_per_reference).
to eq({ project.path_with_namespace => project })
end
end
describe '#find_projects_for_paths' do
it 'returns a list of Projects for a list of paths' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(filter.find_projects_for_paths([project.path_with_namespace])).
to eq([project])
end
end
describe '#current_project_path' do
it 'returns the path of the current project' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(filter.current_project_path).to eq(project.path_with_namespace)
end
end
end
......@@ -19,19 +19,31 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
it 'adds rel="nofollow" to external links' do
act = %q(<a href="https://google.com/">Google</a>)
doc = filter(act)
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'nofollow'
context 'for root links on document' do
let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
it 'adds rel="nofollow" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'nofollow'
end
it 'adds rel="noreferrer" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'noreferrer'
end
end
it 'adds rel="noreferrer" to external links' do
act = %q(<a href="https://google.com/">Google</a>)
doc = filter(act)
context 'for nested links on document' do
let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
it 'adds rel="nofollow" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'nofollow'
end
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'noreferrer'
it 'adds rel="noreferrer" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'noreferrer'
end
end
end
......@@ -25,7 +25,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
expect_any_instance_of(described_class).to receive(:find_object).
with(project, issue.iid).
and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
......@@ -107,8 +109,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference(project) }
it 'ignores valid references when cross-reference project uses external tracker' do
expect_any_instance_of(Project).to receive(:get_issue).
with(issue.iid).and_return(nil)
expect_any_instance_of(described_class).to receive(:find_object).
with(project2, issue.iid).
and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
......
......@@ -23,6 +23,14 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
%(<a href="#{path}">#{path}</a>)
end
def nested_image(path)
%(<div><img src="#{path}" /></div>)
end
def nested_link(path)
%(<div><a href="#{path}">#{path}</a></div>)
end
let(:project) { create(:project) }
shared_examples :preserve_unchanged do
......@@ -47,11 +55,19 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('a')['href']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('a')['href']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
it 'rebuilds relative URL for an image' do
doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('a')['href']).
doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('img')['src']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('img')['src']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
......
......@@ -105,7 +105,8 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('FOOBAR-4567', project)]
end
end
......
class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions_for_signup
user = User.new(name: 'Jane Doe', email: 'signup@example.com')
DeviseMailer.confirmation_instructions(user, 'faketoken', {})
DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
end
def confirmation_instructions_for_new_email
user = User.last
user.unconfirmed_email = 'unconfirmed@example.com'
DeviseMailer.confirmation_instructions(user, 'faketoken', {})
end
def reset_password_instructions
DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
end
def unlock_instructions
DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
end
def password_change
DeviseMailer.password_change(unsaved_user, {})
end
private
def unsaved_user
User.new(name: 'Jane Doe', email: 'jdoe@example.com')
end
end
......@@ -258,6 +258,19 @@ describe Ci::Pipeline, models: true do
end
end
end
context 'when no builds created' do
let(:pipeline) { build(:ci_pipeline) }
before do
stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
end
it 'returns false' do
expect(pipeline.create_builds(nil)).to be_falsey
expect(pipeline).not_to be_persisted
end
end
end
describe "#finished_at" do
......
......@@ -158,6 +158,18 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:requester])).to be_falsey }
end
describe '#owners' do
let(:owner) { create(:user) }
let(:developer) { create(:user) }
it 'returns the owners of a Group' do
group.add_owner(owner)
group.add_developer(developer)
expect(group.owners).to eq([owner])
end
end
def setup_group_members(group)
members = {
owner: create(:user),
......
require 'spec_helper'
describe JiraIssue do
let(:project) { create(:project) }
subject { JiraIssue.new('JIRA-123', project) }
describe 'id' do
subject { super().id }
it { is_expected.to eq('JIRA-123') }
end
describe 'iid' do
subject { super().iid }
it { is_expected.to eq('JIRA-123') }
end
describe 'to_s' do
subject { super().to_s }
it { is_expected.to eq('JIRA-123') }
end
describe :== do
specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) }
specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) }
it 'only compares with JiraIssues' do
expect(subject).not_to eq('JIRA-123')
end
end
end
......@@ -76,7 +76,8 @@ describe JiraService, models: true do
end
it "should call JIRA API" do
@jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
@jira_service.execute(merge_request,
ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
......@@ -84,7 +85,8 @@ describe JiraService, models: true do
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
@jira_service.execute(merge_request,
ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @api_url).with(
body: /this-is-a-custom-id/
).once
......
......@@ -229,7 +229,7 @@ describe Project, models: true do
end
end
describe :find_with_namespace do
describe '.find_with_namespace' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
......@@ -240,6 +240,22 @@ describe Project, models: true do
it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
context 'when multiple projects using a similar name exist' do
let(:group) { create(:group, name: 'gitlab') }
let!(:project1) do
create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
end
let!(:project2) do
create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
end
it 'returns the row where the path matches literally' do
expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
end
end
end
describe :to_param do
......
......@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
described_class.new(pipeline).execute('test', nil, user, status)
described_class.new(pipeline).execute('test', user, status, nil)
end
context 'next builds available' do
......@@ -17,6 +17,10 @@ describe Ci::CreateBuildsService, services: true do
it { is_expected.to be_an_instance_of Array }
it { is_expected.to all(be_an_instance_of Ci::Build) }
it 'does not persist created builds' do
expect(subject.first).not_to be_persisted
end
end
context 'builds skipped' do
......
......@@ -39,7 +39,7 @@ describe CreateCommitBuildsService, services: true do
end
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
stub_ci_pipeline_yaml_file(config)
result = service.execute(project, user,
......@@ -81,7 +81,7 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.yaml_errors).not_to be_nil
end
describe :ci_skip? do
context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
before do
......@@ -171,5 +171,24 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.status).to eq("failed")
expect(pipeline.builds.any?).to be false
end
context 'when there are no jobs for this pipeline' do
before do
config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
stub_ci_pipeline_yaml_file(config)
end
it 'does not create a new pipeline' do
result = service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
after: '31das312',
commits: [{ message: 'some msg' }])
expect(result).to be_falsey
expect(Ci::Build.all).to be_empty
expect(Ci::Pipeline.count).to eq(0)
end
end
end
end
......@@ -328,7 +328,8 @@ describe GitPushService, services: true do
end
it "doesn't close issues when external issue tracker is in use" do
allow(project).to receive(:default_issues_tracker?).and_return(false)
allow_any_instance_of(Project).to receive(:default_issues_tracker?).
and_return(false)
# The push still shouldn't create cross-reference notes.
expect do
......
......@@ -536,7 +536,7 @@ describe SystemNoteService, services: true do
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { JiraIssue.new("JIRA-1", project)}
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
let(:commit) { project.commit }
......
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