Commit 1e2dd2ac authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into 3776-ci-view-for-sast

* master: (73 commits)
  Fixes broken modal in DAST report
  Use more specific avatar_icon_for_user in EE.
  Extract repeated logic into #avatar_icon_for.
  Add changelog.
  Always eagerly load a note's author.
  Always eagerly load an issue's author.
  Extract method to improve readability.
  Remove generic #avatar_icon helper.
  Explicit use of avatar_icon_* calls depending on situation.
  Use avatar_icon_for_email for commits.
  Retrieve project creator's avatar by creator, not creator#email.
  Retrieve namespace owner's avatar by owner, not owner#email.
  Retrieve issue author's avatar by user, not user#email.
  Use more specific #avatar_icon_for_email.
  Use more specific #avatar_icon_for_user.
  Refactor and split ApplicationHelper#avatar_icon.
  EE port of pipeline-schedule-webpack
  Only check LFS integrity for first branch in push
  Improve validation message and add changelog
  Refactor weight select
  ...
parents fff8071a 46a437fd
......@@ -10,7 +10,6 @@ import SearchAutocomplete from './search_autocomplete';
import UsersSelect from './users_select';
import UserCallout from './user_callout';
import ZenMode from './zen_mode';
import initCompareAutocomplete from './compare_autocomplete';
import initGeoInfoModal from 'ee/init_geo_info_modal'; // eslint-disable-line import/first
import initGroupAnalytics from 'ee/init_group_analytics'; // eslint-disable-line import/first
import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first
......@@ -53,19 +52,6 @@ var Dispatcher;
});
});
function initBlobEE() {
const dataEl = document.getElementById('js-file-lock');
if (dataEl) {
const {
toggle_path,
path,
} = JSON.parse(dataEl.innerHTML);
initPathLocks(toggle_path, path);
}
}
switch (page) {
case 'projects:environments:metrics':
import('./pages/projects/environments/metrics')
......@@ -86,7 +72,6 @@ var Dispatcher;
import('./pages/projects/milestones/show')
.then(callDefault)
.catch(fail);
new UserCallout();
break;
case 'groups:milestones:show':
import('./pages/groups/milestones/show')
......@@ -185,14 +170,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'groups:epics:show':
new ZenMode();
break;
case 'groups:epics:index':
import(/* webpackChunkName: "ee_epics_show" */ 'ee/pages/epics')
.then(callDefault)
.catch(fail);
break;
case 'projects:compare:show':
import('./pages/projects/compare/show')
.then(callDefault)
......@@ -229,24 +206,17 @@ var Dispatcher;
import('./pages/projects/merge_requests/creations/new')
.then(callDefault)
.catch(fail);
new UserCallout();
case 'projects:merge_requests:creations:diffs':
import('./pages/projects/merge_requests/creations/diffs')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
// ee-start
initApprovals();
// ee-end
break;
case 'projects:merge_requests:edit':
import('./pages/projects/merge_requests/edit')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
// ee-start
initApprovals();
// ee-end
break;
case 'projects:tags:new':
import('./pages/projects/tags/new')
......@@ -331,9 +301,6 @@ var Dispatcher;
break;
case 'projects:show':
shortcut_handler = true;
// ee-start
initGeoInfoModal();
// ee-end
break;
case 'projects:edit':
import(/* webpackChunkName: "ee_projects_edit" */ 'ee/pages/projects/edit')
......@@ -415,14 +382,12 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
shortcut_handler = true;
initBlobEE();
break;
case 'projects:blame:show':
import('./pages/projects/blame/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
initBlobEE();
break;
case 'groups:labels:new':
import('./pages/groups/labels/new')
......@@ -485,26 +450,11 @@ var Dispatcher;
import('./pages/search/show')
.then(callDefault)
.catch(fail);
new UserCallout();
break;
case 'projects:mirrors:show':
case 'projects:mirrors:update':
new UsersSelect();
break;
case 'admin:emails:show':
import(/* webpackChunkName: "ee_admin_emails_show" */ 'ee/pages/admin/emails/show').then(m => m.default()).catch(fail);
break;
case 'admin:audit_logs:index':
import(/* webpackChunkName: "ee_audit_logs" */ 'ee/pages/admin/audit_logs').then(m => m.default()).catch(fail);
break;
case 'projects:settings:repository:show':
import('./pages/projects/settings/repository/show')
.then(callDefault)
.catch(fail);
// ee-start
new UsersSelect();
new UserCallout();
// ee-end
break;
case 'projects:settings:ci_cd:show':
import('./pages/projects/settings/ci_cd/show')
......@@ -552,11 +502,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'profiles:personal_access_tokens:index':
import('./pages/profiles/personal_access_tokens')
.then(callDefault)
.catch(fail);
break;
case 'projects:clusters:show':
case 'projects:clusters:update':
case 'projects:clusters:destroy':
......@@ -573,14 +518,6 @@ var Dispatcher;
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
case 'admin:licenses:new':
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
case 'groups:analytics:show':
initGroupAnalytics();
break;
case 'groups:ldap_group_links:index':
initLDAPGroupsSelect();
break;
}
switch (path[0]) {
......@@ -616,11 +553,7 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
......@@ -645,11 +578,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'geo_nodes':
import(/* webpackChunkName: 'geo_node_form' */ './geo/geo_node_form')
.then(geoNodeForm => geoNodeForm.default($('.js-geo-node-form')))
.catch(() => {});
break;
}
break;
case 'profiles':
......@@ -664,7 +592,9 @@ var Dispatcher;
shortcut_handler = true;
switch (path[1]) {
case 'compare':
initCompareAutocomplete();
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
......@@ -686,6 +616,81 @@ var Dispatcher;
new Shortcuts();
}
// EE-only route-based code
function initBlobEE() {
const dataEl = document.getElementById('js-file-lock');
if (dataEl) {
const {
toggle_path,
path,
} = JSON.parse(dataEl.innerHTML);
initPathLocks(toggle_path, path);
}
}
switch (page) {
case 'groups:epics:show':
new ZenMode();
break;
case 'groups:epics:index':
import(/* webpackChunkName: "ee_epics_index" */ 'ee/pages/epics')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:show':
case 'search:show':
new UserCallout();
break;
case 'projects:merge_requests:creations:new':
new UserCallout();
initApprovals();
break;
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
initApprovals();
break;
case 'projects:show':
initGeoInfoModal();
break;
case 'projects:blob:show':
case 'projects:blame:show':
initBlobEE();
break;
case 'projects:mirrors:show':
case 'projects:mirrors:update':
new UsersSelect();
break;
case 'admin:emails:show':
import(/* webpackChunkName: "ee_admin_emails_show" */ 'ee/pages/admin/emails/show').then(m => m.default()).catch(fail);
break;
case 'admin:audit_logs:index':
import(/* webpackChunkName: "ee_audit_logs" */ 'ee/pages/admin/audit_logs').then(m => m.default()).catch(fail);
break;
case 'projects:settings:repository:show':
new UsersSelect();
new UserCallout();
break;
case 'admin:licenses:new':
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
case 'groups:analytics:show':
initGroupAnalytics();
break;
case 'groups:ldap_group_links:index':
initLDAPGroupsSelect();
break;
case 'admin:groups:edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
break;
case 'admin:geo_nodes':
import(/* webpackChunkName: 'geo_node_form' */ './geo/geo_node_form')
.then(geoNodeForm => geoNodeForm.default($('.js-geo-node-form')))
.catch(() => {});
break;
}
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
......
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
......
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import Translate from '../../../../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
......
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import GlFieldErrors from '../gl_field_errors';
import Translate from '../../../../vue_shared/translate';
import GlFieldErrors from '../../../../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list';
import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
......@@ -27,7 +27,7 @@ function initIntervalPatternInput() {
});
}
document.addEventListener('DOMContentLoaded', () => {
export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
......@@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => {
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
});
});
};
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="success"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Ready to be merged automatically.
Ask someone with write access to this repository to merge this request`) }}
</span>
</div>
</div>
</template>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Pipeline blocked.
The pipeline for this merge request requires a manual action to proceed`) }}
</span>
</div>
</div>
</template>
......@@ -25,11 +25,11 @@ export { default as ArchivedState } from './components/states/mr_widget_archived
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
......
......@@ -805,10 +805,19 @@
margin: 0;
line-height: $code_line_height;
.btn-open-modal {
padding: 0 5px 4px;
}
.mr-widget-code-quality-list-item {
display: flex;
}
.mr-widget-code-quality-list-item-modal {
display: flex;
flex-wrap: wrap;
}
.failed .mr-widget-code-quality-icon {
color: $red-500;
}
......
......@@ -21,7 +21,7 @@ class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
def klass
Issue
Issue.includes(:author)
end
def with_confidentiality_access_check
......
......@@ -57,7 +57,7 @@ class NotesFinder
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map! { |notes| search(notes) }
UnionFinder.new.find_union(note_relations, Note)
UnionFinder.new.find_union(note_relations, Note.includes(:author))
end
def noteables_for_type(noteable_type)
......
......@@ -68,18 +68,32 @@ module ApplicationHelper
end
end
def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true)
user =
if user_or_email.is_a?(User)
user_or_email
else
User.find_by_any_email(user_or_email.try(:downcase))
end
# Takes both user and email and returns the avatar_icon by
# user (preferred) or email.
def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
elsif email
avatar_icon_for_email(email, size, scale, only_path: only_path)
else
default_avatar
end
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase))
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
gravatar_icon(email, size, scale)
end
end
def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
if user
user.avatar_url(size: size, only_path: only_path) || default_avatar
else
gravatar_icon(user_or_email, size, scale)
gravatar_icon(nil, size, scale)
end
end
......
......@@ -8,10 +8,22 @@ module AvatarsHelper
}))
end
def user_avatar_url_for(options = {})
if options[:url]
options[:url]
elsif options[:user]
avatar_icon_for_user(options[:user], options[:size])
else
avatar_icon_for_email(options[:user_email], options[:size])
end
end
def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
avatar_url = user_avatar_url_for(options.merge(size: avatar_size))
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
......
......@@ -35,7 +35,7 @@ module NamespacesHelper
if namespace.is_a?(Group)
group_icon(namespace)
else
avatar_icon(namespace.owner.email, size)
avatar_icon_for_user(namespace.owner, size)
end
end
......
......@@ -21,7 +21,7 @@ module ProjectsHelper
classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class]
avatar = avatar_icon(author, opts[:size])
avatar = avatar_icon_for_user(author, opts[:size])
src = opts[:lazy_load] ? nil : avatar
image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
......@@ -298,6 +298,10 @@ module ProjectsHelper
nav_tabs << :pipelines
end
if project.external_issue_tracker
nav_tabs << :external_issue_tracker
end
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
......
......@@ -317,7 +317,7 @@ class Member < ActiveRecord::Base
end
def notification_setting
@notification_setting ||= user.notification_settings_for(source)
@notification_setting ||= user&.notification_settings_for(source)
end
def notifiable?(type, opts = {})
......
......@@ -266,7 +266,7 @@ class Project < ActiveRecord::Base
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: true
validates :variables, variable_duplicates: { scope: :environment_scope }
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -10,6 +10,8 @@ class JiraService < IssueTrackerService
before_update :reset_password
alias_method :project_url, :url
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
......
......@@ -11,6 +11,7 @@ module Members
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
member.destroy
end
......
# VariableDuplicatesValidator
#
# This validtor is designed for especially the following condition
# This validator is designed for especially the following condition
# - Use `accepts_nested_attributes_for :xxx` in a parent model
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if options[:scope]
scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
end
scoped.each_value { |scope| validate_duplicates(record, attribute, scope) }
else
validate_duplicates(record, attribute, value)
end
end
private
def validate_duplicates(record, attribute, values)
duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first)
if duplicates.any?
record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}")
error_message = "have duplicate values (#{duplicates.join(", ")})"
error_message += " for #{values.first.send(options[:scope])} scope" if options[:scope] # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, error_message)
end
end
end
%li.flex-row
.user-avatar
= image_tag avatar_icon(user), class: "avatar", alt: ''
= image_tag avatar_icon_for_user(user), class: "avatar", alt: ''
.row-main-content
.user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user]
......
......@@ -10,7 +10,7 @@
= @user.name
%ul.well-list
%li
= image_tag avatar_icon(@user, 60), class: "avatar s60"
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
%span.light Profile page:
%strong
......
......@@ -3,7 +3,7 @@
.timeline-entry-inner
.timeline-icon
= link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40"
= image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ data: { discussion_id: discussion.id } }
.discussion-header
......
......@@ -10,7 +10,7 @@ xml.entry do
# eager-loaded. This allows us to re-use the user object's Email address,
# instead of having to run additional queries to figure out what Email to use
# for the avatar.
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author))
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(event.author))
xml.author do
xml.username event.author_username
......
......@@ -68,7 +68,7 @@
.example
.cover-block
.avatar-holder
= image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
= image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title
John Smith
......
......@@ -3,7 +3,7 @@ xml.entry do
xml.link href: project_issue_url(issue.project, issue)
xml.title truncate(issue.title, length: 80)
xml.updated issue.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(issue.author))
xml.author do
xml.name issue.author_name
......
......@@ -45,7 +45,7 @@
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
......
......@@ -141,6 +141,19 @@
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
Milestones
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
= link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('issue-external')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path do
%strong.fly-out-top-item-name
= issue_tracker.title
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
......
......@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
......@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
......@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
......
......@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
......@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
......@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
......
......@@ -20,8 +20,8 @@
or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
.col-lg-8
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0= _("Upload new avatar")
.prepend-top-5.append-bottom-10
%button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
......
......@@ -3,7 +3,7 @@ xml.entry do
xml.link href: project_commit_url(@project, id: commit.id)
xml.title truncate(commit.title, length: 80, escape: false)
xml.updated commit.committed_date.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email))
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_email(commit.author_email))
xml.author do |author|
xml.name commit.author_name
......
by
%a{ href: user_path(@build.user) }
%span.hidden-xs
= image_tag avatar_icon(@build.user, 24), class: "avatar s24"
= image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24"
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name
%strong.visible-xs-inline= @build.user.to_reference
......@@ -9,7 +9,7 @@
author: {
name: c.author_name,
email: c.author_email,
icon: image_path(avatar_icon(c.author_email, 20))
icon: image_path(avatar_icon_for_email(c.author_email, 20))
},
time: c.time,
space: c.spaces.first,
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedule_form'
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
......
......@@ -21,7 +21,7 @@
= s_("PipelineSchedules|Inactive")
%td
- if pipeline_schedule.owner
= image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20"
= image_tag avatar_icon_for_user(pipeline_schedule.owner, 20), class: "avatar s20"
= link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name
%td
......
- breadcrumb_title _("Schedules")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedules_index'
- @no_container = true
- page_title _("Pipeline Schedules")
......
......@@ -11,7 +11,7 @@
%div
= link_to project_commits_path(@project, commit.id) do
%code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
= image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: ''
= markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td
%span.pull-right.cgray
......
......@@ -7,7 +7,7 @@
= snippet.title
by
= link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: ''
= image_tag avatar_icon_for_user(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
......
......@@ -18,6 +18,6 @@
%span
by
= link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= image_tag avatar_icon_for_user(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at)
......@@ -8,7 +8,7 @@
%li.member{ class: [dom_class(member), ("is-overriden" if member.override)], id: dom_id(member) }
%span.list-item-name
- if user
= image_tag avatar_icon(user, 40), class: "avatar s40", alt: ''
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info
= link_to user.name, user_path(user), class: 'member'
%span.cgray= user.to_reference
......@@ -36,7 +36,7 @@
Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
= image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40", alt: ''
.user-info
.member= member.invite_email
.cgray
......
......@@ -28,4 +28,4 @@
- assignees.each do |assignee|
= link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
- image_tag(avatar_icon_for_user(assignee, 16), class: "avatar s16", alt: '')
......@@ -2,7 +2,7 @@
- users.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
= image_tag avatar_icon_for_user(user, 32), class: "avatar s32"
%strong= truncate(user.name, length: 40)
%div
%small.cgray= user.username
......@@ -16,7 +16,7 @@
= icon_for_system_note(note)
- else
%a.image-diff-avatar-link{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
= image_tag avatar_icon_for_user(note.author), alt: '', class: 'avatar s40'
- if note.is_a?(DiffNote) && note.on_image?
- if show_image_comment_badge && note_counter == 0
-# Only show this for the first comment in the discussion
......
......@@ -14,7 +14,7 @@
.timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
......
......@@ -17,7 +17,7 @@
.avatar-container.s40
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
= image_tag avatar_icon_for_user(project.creator, 40), class: "avatar s40", alt:''
- else
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
......
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row
= image_tag avatar_icon(snippet.author), class: "avatar s40 hidden-xs", alt: ''
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
......
......@@ -35,8 +35,8 @@
.profile-header
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
.user-info
.cover-title
......
---
title: "[Geo] Fix redownload repository recovery when there is not local repo at all"
merge_request:
author:
type: fixed
---
title: Fix broken CSS in modal for DAST report
merge_request:
author:
type: fixed
---
title: Fix validation of environment scope of variables
merge_request:
author:
type: fixed
---
title: Display a link to external issue tracker when enabled
merge_request:
author:
type: changed
---
title: Remove user notification settings for groups and projects when user leaves
merge_request: 16906
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Use a user object in ApplicationHelper#avatar_icon where possible to avoid
N+1 queries.
merge_request: 42800
author:
type: performance
---
title: 'API: Get references a commit is pushed to'
merge_request: 15026
author: Robert Schilling
type: added
---
title: Only check LFS integrity for first ref in a push to avoid timeout
merge_request: 17098
author:
type: performance
......@@ -105,8 +105,6 @@ var config = {
ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js',
ee_sidebar: 'ee/sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
......@@ -173,7 +171,7 @@ var config = {
include: /node_modules\/katex\/dist/,
use: [
{ loader: 'style-loader' },
{
{
loader: 'css-loader',
options: {
name: '[name].[hash].[ext]'
......
......@@ -198,6 +198,41 @@ Example response:
}
```
## Get references a commit is pushed to
> [Introduced][ce-15026] in GitLab 10.6
Get all references (from branches or tags) a commit is pushed to.
The pagination parameters `page` and `per_page` can be used to restrict the list of references.
```
GET /projects/:id/repository/commits/:sha/refs
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash |
| `type` | string | no | The scope of commits. Possible values `branch`, `tag`, `all`. Default is `all`. |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/5937ac0a7beb003549fc5fd26fc247adbce4a52e/refs?type=all"
```
Example response:
```json
[
{"type": "branch", "name": "'test'"},
{"type": "branch", "name": "add-balsamiq-file"},
{"type": "branch", "name": "wip"},
{"type": "tag", "name": "v1.1.0"}
]
```
## Cherry pick a commit
> [Introduced][ce-8047] in GitLab 8.15.
......@@ -500,3 +535,4 @@ Example response:
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
......@@ -39,7 +39,7 @@ Example response:
"path": "group1",
"kind": "group"
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2,
"plan": "bronze"
},
......@@ -49,7 +49,7 @@ Example response:
"path": "bar",
"kind": "group",
"full_path": "foo/bar",
"parent_id": "9",
"parent_id": 9,
"members_count_with_descendants": 5
}
]
......@@ -85,7 +85,7 @@ Example response:
"path": "twitter",
"kind": "group",
"full_path": "twitter",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
]
......@@ -118,7 +118,7 @@ Example response:
"path": "group1",
"kind": "group",
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
```
......@@ -138,7 +138,7 @@ Example response:
"path": "group1",
"kind": "group",
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
```
......@@ -11,11 +11,7 @@ in the table below.
| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page
- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
Once you have configured and enabled Bugzilla you'll see the Bugzilla link on the GitLab project pages that takes you to the appropriate Bugzilla project.
## Referencing issues in Bugzilla
......
......@@ -118,7 +118,7 @@ in the table below.
| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
After saving the configuration, your GitLab project will be able to interact
with all JIRA projects in your JIRA instance.
with all JIRA projects in your JIRA instance and you'll see the JIRA link on the GitLab project pages that takes you to the appropriate JIRA project.
![JIRA service page](img/jira_service_page.png)
......
......@@ -12,6 +12,8 @@ in the table below.
| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** |
Once you have configured and enabled Redmine you'll see the Redmine link on the GitLab project pages that takes you to the appropriate Redmine project.
As an example, below is a configuration for a project named gitlab-ci.
![Redmine configuration](img/redmine_configuration.png)
......
<script>
import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import expandButton from '~/vue_shared/components/expand_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
export default {
name: 'ModalDast',
components: {
expandButton,
ExpandButton,
Icon,
},
props: {
title: {
......@@ -27,13 +28,10 @@
instances: {
type: Array,
required: false,
default: () => [],
default: () => ([]),
},
},
computed: {
cutIcon() {
return spriteIcon('cut');
},
instancesLabel() {
return s__('ciReport|Instances');
},
......@@ -54,7 +52,7 @@
role="dialog"
>
<div
class="modal-dialog"
class="modal-dialog modal-lg"
role="document"
>
<div class="modal-content">
......@@ -83,18 +81,21 @@
<li
v-for="(instance, i) in instances"
:key="i"
class="failed"
class="mr-widget-code-quality-list-item-modal failed"
>
<span
<icon
class="mr-widget-code-quality-icon"
v-html="cutIcon"
>
</span>
name="status_failed_borderless"
:size="32"
/>
{{ instance.method }}
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ instance.uri }}
</a>
......
......@@ -143,6 +143,7 @@
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.name }}
</a>
......@@ -155,7 +156,7 @@
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="btn-link btn-blank"
class="btn-link btn-blank btn-open-modal"
:data-target="modalTargetId"
>
{{ issue.name }}
......
......@@ -20,30 +20,6 @@ function WeightSelect(els, options = {}) {
inputField.val(options.selected);
}
updateWeight = function(selected) {
var data;
data = {};
data[abilityName] = {};
data[abilityName].weight = selected != null ? selected : null;
$loading.fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
dataType: 'json',
url: updateUrl,
data: data
}).done(function(data) {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectbox.hide();
if (data.weight != null) {
$value.html(`<strong>${data.weight}</strong>`);
} else {
$value.html('<span class="no-value">None</span>');
}
return $sidebarCollapsedValue.html(data.weight);
});
};
return $dropdown.glDropdown({
selectable: true,
fieldName,
......@@ -70,13 +46,8 @@ function WeightSelect(els, options = {}) {
e.preventDefault();
selected = inputField.val();
options.handleClick(selected);
} else if ($(dropdown).is(".js-filter-submit")) {
return $(dropdown).parents('form').submit();
} else if ($dropdown.is('.js-issuable-form-weight')) {
e.preventDefault();
} else {
selected = inputField.val();
return updateWeight(selected);
}
}
});
......
......@@ -8,7 +8,7 @@ module EpicsHelper
name: author.name,
url: user_path(author),
username: "@#{author.username}",
src: avatar_icon(@epic.author)
src: avatar_icon_for_user(@epic.author)
},
start_date: @epic.start_date,
end_date: @epic.end_date
......
......@@ -184,8 +184,7 @@ module Geo
# Remove the deleted path in case it exists, but it may not be there
gitlab_shell.remove_repository(project.repository_storage_path, deleted_disk_path_temp)
# Move the original repository out of the way
unless gitlab_shell.mv_repository(project.repository_storage_path, repository.disk_path, deleted_disk_path_temp)
if project.repository_exists? && !gitlab_shell.mv_repository(project.repository_storage_path, repository.disk_path, deleted_disk_path_temp)
raise Gitlab::Shell::Error, 'Can not move original repository out of the way'
end
......
......@@ -79,7 +79,7 @@
%span{ style: "font-weight: 600;color:#333333;" } Merge request
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
%span was approved by
%img.avatar{ height: "24", src: avatar_icon(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" }
= @approved_by.name
%tr.spacer
......@@ -117,7 +117,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
......@@ -130,7 +130,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(@merge_request.assignee, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.assignee, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" }
= @merge_request.assignee.name
......
......@@ -79,7 +79,7 @@
%span{ style: "font-weight: 600;color:#333333;" } Merge request
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
%span was unapproved by
%img.avatar{ height: "24", src: avatar_icon(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
%a.muted{ href: user_url(@unapproved_by), style: "color:#333333;text-decoration:none;" }
= @unapproved_by.name
%tr.spacer
......@@ -117,7 +117,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
......@@ -128,7 +128,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(@merge_request.assignee, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.assignee, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" }
= @merge_request.assignee.name
......
......@@ -141,7 +141,7 @@ describe Groups::EpicsController do
show_epic(:json)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('entities/epic')
expect(response).to match_response_schema('entities/epic', dir: 'ee')
end
context 'with unauthorized user' do
......
require_relative '../support/test_env'
FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
......
......@@ -13,7 +13,7 @@ describe EpicsHelper do
'name' => user.name,
'url' => "/#{user.username}",
'username' => "@#{user.username}",
'src' => "#{avatar_icon(user)}"
'src' => "#{avatar_icon_for_user(user)}"
})
end
end
......
......@@ -6,7 +6,7 @@ describe EE::Gitlab::Ci::Config do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- /spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
- /ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
- #{remote_location}
image: ruby:2.2
......@@ -38,7 +38,7 @@ describe EE::Gitlab::Ci::Config do
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
HEREDOC
end
let(:local_file_content) { File.read("#{Rails.root}/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml") }
let(:local_file_content) { File.read(Rails.root.join('ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) }
before do
allow(project).to receive(:feature_available?).with(:external_files_in_gitlab_ci).and_return(true)
......
......@@ -107,7 +107,7 @@ describe Gitlab::Ci::External::Processor do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
let(:external_files) do
[
'/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml',
'/ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml',
remote_file
]
end
......@@ -128,7 +128,7 @@ describe Gitlab::Ci::External::Processor do
end
before do
local_file_content = File.read("#{Rails.root}/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml")
local_file_content = File.read(Rails.root.join('ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml'))
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content)
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
end
......
......@@ -8,7 +8,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
stub_config_setting(host: 'localhost')
end
let(:email_raw) { fixture_file('emails/service_desk.eml') }
let(:email_raw) { fixture_file('emails/service_desk.eml', dir: 'ee') }
let(:namespace) { create(:namespace, name: "email") }
context 'service desk is enabled for the project' do
......@@ -64,7 +64,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
end
context 'when the email is forwarded through an alias' do
let(:email_raw) { fixture_file('emails/service_desk_forwarded.eml') }
let(:email_raw) { fixture_file('emails/service_desk_forwarded.eml', dir: 'ee') }
it 'sends thank you the email and creates issue' do
setup_attachment
......
require 'spec_helper'
describe Gitlab::Email::Handler do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
describe '.for' do
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
def ee_handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture, dir: 'ee'), mail_key)
end
context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(ee_handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end
it 'uses no handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(ee_handler_for('emails/service_desk.eml', 'some/project')).to be_nil
end
end
context 'a new issue email' do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
end
......@@ -33,7 +33,7 @@ describe SystemCheck::Geo::AuthorizedKeysCheck do
it 'skips when config file is not readable' do
override_sshd_config('system_check/sshd_config')
allow(File).to receive(:readable?).with(expand_fixture_ee_path('system_check/sshd_config')) { false }
allow(File).to receive(:readable?).with(expand_fixture_path('system_check/sshd_config', dir: 'ee')) { false }
expect_skipped('Cannot access OpenSSH configuration file')
......@@ -167,6 +167,6 @@ describe SystemCheck::Geo::AuthorizedKeysCheck do
end
def override_sshd_config(relative_path)
allow(subject).to receive(:openssh_config_path) { expand_fixture_ee_path(relative_path) }
allow(subject).to receive(:openssh_config_path) { expand_fixture_path(relative_path, dir: 'ee') }
end
end
......@@ -19,6 +19,32 @@ describe Project do
it { is_expected.to have_many(:audit_events).dependent(false) }
end
describe 'validations' do
let(:project) { build(:project) }
describe 'variables' do
let(:first_variable) { build(:ci_variable, key: 'test_key', value: 'first', environment_scope: 'prod', project: project) }
let(:second_variable) { build(:ci_variable, key: 'test_key', value: 'other', environment_scope: 'other', project: project) }
before do
project.variables << first_variable
project.variables << second_variable
end
context 'with duplicate variables with same environment scope' do
before do
project.variables.last.environment_scope = project.variables.first.environment_scope
end
it { expect(project).not_to be_valid }
end
context 'with same variable keys and different environment scope' do
it { expect(project).to be_valid }
end
end
end
describe '.mirrors_to_sync' do
let(:timestamp) { Time.now }
......
......@@ -56,7 +56,7 @@ describe API::V3::Github do
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(2)
expect(response).to match_response_schema('entities/github/repositories')
expect(response).to match_response_schema('entities/github/repositories', dir: 'ee')
end
it 'returns valid project path as name' do
......@@ -104,7 +104,7 @@ describe API::V3::Github do
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(response).to match_response_schema('entities/github/branches')
expect(response).to match_response_schema('entities/github/branches', dir: 'ee')
end
end
......@@ -142,7 +142,7 @@ describe API::V3::Github do
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('entities/github/commit')
expect(response).to match_response_schema('entities/github/commit', dir: 'ee')
end
end
......
......@@ -116,4 +116,103 @@ describe AuditEventService do
end
end
end
describe '#for_failed_login' do
let(:author_name) { 'testuser' }
let(:ip_address) { '127.0.0.1' }
let(:service) { described_class.new(author_name, nil, ip_address: ip_address) }
let(:event) { service.for_failed_login.unauth_security_event }
before do
stub_licensed_features(extended_audit_events: true)
end
it 'has the right type' do
expect(event.entity_type).to eq('User')
end
it 'has the right author' do
expect(event.details[:author_name]).to eq(author_name)
end
it 'has the right IP address' do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
expect(event.details[:ip_address]).to eq(ip_address)
end
it 'has the right auth method for OAUTH' do
oauth_service = described_class.new(author_name, nil, ip_address: ip_address, with: 'ldap')
event = oauth_service.for_failed_login.unauth_security_event
expect(event.details[:failed_login]).to eq('LDAP')
end
end
describe 'license' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:service) { described_class.new(user, project, action: :create) }
let(:event) { service.for_project.security_event }
before do
disable_license_audit_features(service)
end
describe 'has the audit_admin feature' do
before do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has the entity_path' do
expect(event.details[:entity_path]).to eq(project.full_path)
end
end
describe 'has the extended_audit_events feature' do
before do
allow(service).to receive(:entity_audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'entity has the audit_events feature' do
before do
allow(service).to receive(:audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'has not any audit event feature' do
it 'does not log the audit event' do
expect { event }.not_to change(AuditEvent, :count)
end
end
def disable_license_audit_features(service)
[:entity_audit_events_enabled?,
:admin_audit_log_enabled?,
:audit_events_enabled?].each do |f|
allow(service).to receive(f).and_return(false)
end
end
end
end
require 'spec_helper'
describe Ci::RegisterJobService do
let!(:project) { create :project, shared_runners_enabled: false }
let!(:pipeline) { create :ci_empty_pipeline, project: project }
let!(:pending_build) { create :ci_build, pipeline: pipeline }
let(:shared_runner) { create(:ci_runner, :shared) }
describe '#execute' do
context 'for project with shared runners when global minutes limit is set' do
before do
project.update(shared_runners_enabled: true)
stub_application_setting(shared_runners_minutes: 100)
end
context 'allow to pick builds' do
let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Ci::Build) }
end
context 'when over the global quota' do
before do
project.namespace.create_namespace_statistics(
shared_runners_seconds: 6001)
end
let(:build) { execute(shared_runner) }
it "does not return a build" do
expect(build).to be_nil
end
context 'when project is public' do
before do
project.update(visibility_level: Project::PUBLIC)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
context 'when namespace limit is set to unlimited' do
before do
project.namespace.update(shared_runners_minutes_limit: 0)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
context 'when namespace quota is bigger than a global one' do
before do
project.namespace.update(shared_runners_minutes_limit: 101)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
end
context 'when group is subgroup' do
let!(:root_ancestor) { create(:group) }
let!(:group) { create(:group, parent: root_ancestor) }
let!(:project) { create :project, shared_runners_enabled: true, group: group }
let(:build) { execute(shared_runner) }
context 'when shared_runner_minutes_on_root_namespace is disabled' do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: false)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
context 'when shared_runner_minutes_on_root_namespace is enabled', :nested_groups do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: true)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "limit is ignored and build is returned" do
expect(build).not_to be_nil
end
end
context 'when we are over limit on root namespace' do
before do
root_ancestor.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
end
end
def execute(runner)
described_class.new(runner).execute.build
end
end
end
require 'spec_helper'
describe AuditEventService do
describe '#for_failed_login' do
let(:author_name) { 'testuser' }
let(:ip_address) { '127.0.0.1' }
let(:service) { described_class.new(author_name, nil, ip_address: ip_address) }
let(:event) { service.for_failed_login.unauth_security_event }
before do
stub_licensed_features(extended_audit_events: true)
end
it 'has the right type' do
expect(event.entity_type).to eq('User')
end
it 'has the right author' do
expect(event.details[:author_name]).to eq(author_name)
end
it 'has the right IP address' do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
expect(event.details[:ip_address]).to eq(ip_address)
end
it 'has the right auth method for OAUTH' do
oauth_service = described_class.new(author_name, nil, ip_address: ip_address, with: 'ldap')
event = oauth_service.for_failed_login.unauth_security_event
expect(event.details[:failed_login]).to eq('LDAP')
end
end
describe 'license' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:service) { described_class.new(user, project, action: :create) }
let(:event) { service.for_project.security_event }
before do
disable_license_audit_features(service)
end
describe 'has the audit_admin feature' do
before do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has the entity_path' do
expect(event.details[:entity_path]).to eq(project.full_path)
end
end
describe 'has the extended_audit_events feature' do
before do
allow(service).to receive(:entity_audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'entity has the audit_events feature' do
before do
allow(service).to receive(:audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'has not any audit event feature' do
it 'does not log the audit event' do
expect { event }.not_to change(AuditEvent, :count)
end
end
def disable_license_audit_features(service)
[:entity_audit_events_enabled?,
:admin_audit_log_enabled?,
:audit_events_enabled?].each do |f|
allow(service).to receive(f).and_return(false)
end
end
end
end
require 'spec_helper'
module Ci
describe RegisterJobService do
let!(:project) { create :project, shared_runners_enabled: false }
let!(:pipeline) { create :ci_empty_pipeline, project: project }
let!(:pending_build) { create :ci_build, pipeline: pipeline }
let(:shared_runner) { create(:ci_runner, :shared) }
describe '#execute' do
context 'for project with shared runners when global minutes limit is set' do
before do
project.update(shared_runners_enabled: true)
stub_application_setting(shared_runners_minutes: 100)
end
context 'allow to pick builds' do
let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Build) }
end
context 'when over the global quota' do
before do
project.namespace.create_namespace_statistics(
shared_runners_seconds: 6001)
end
let(:build) { execute(shared_runner) }
it "does not return a build" do
expect(build).to be_nil
end
context 'when project is public' do
before do
project.update(visibility_level: Project::PUBLIC)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace limit is set to unlimited' do
before do
project.namespace.update(shared_runners_minutes_limit: 0)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace quota is bigger than a global one' do
before do
project.namespace.update(shared_runners_minutes_limit: 101)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
end
context 'when group is subgroup' do
let!(:root_ancestor) { create(:group) }
let!(:group) { create(:group, parent: root_ancestor) }
let!(:project) { create :project, shared_runners_enabled: true, group: group }
let(:build) { execute(shared_runner) }
context 'when shared_runner_minutes_on_root_namespace is disabled' do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: false)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
context 'when shared_runner_minutes_on_root_namespace is enabled', :nested_groups do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: true)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "limit is ignored and build is returned" do
expect(build).not_to be_nil
end
end
context 'when we are over limit on root namespace' do
before do
root_ancestor.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
end
end
def execute(runner)
described_class.new(runner).execute.build
end
end
end
end
......@@ -247,7 +247,7 @@ describe Geo::RepositorySyncService do
subject.execute
end
it 'successfully redownloads the file even if the retry time exceeds max value' do
it 'successfully redownloads the repository even if the retry time exceeds max value' do
timestamp = Time.now.utc
registry = create(
:geo_project_registry,
......@@ -265,6 +265,20 @@ describe Geo::RepositorySyncService do
registry.reload
expect(registry.repository_retry_at).to be_nil
end
context 'no repository' do
let(:project) { create(:project) }
it 'does not raise an error' do
create(
:geo_project_registry,
project: project,
force_to_redownload_repository: true
)
subject.execute
end
end
end
end
end
RSpec.configure do |config|
config.before(:suite) do
FactoryBot.definition_file_paths = [
Rails.root.join('ee', 'spec', 'factories')
]
FactoryBot.find_definitions
end
end
......@@ -26,17 +26,24 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods
include ActiveJob::TestHelper
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
RSpec::Mocks.setup
TestEnv.init(mailer: false)
# EE-specific START
FactoryBot.definition_file_paths = [
Rails.root.join('ee', 'spec', 'factories')
]
FactoryBot.find_definitions
License.destroy_all
TestLicense.init
# EE-specific END
# skip pre-receive hook check so we can use
# web editor and merge
TestEnv.disable_pre_receive
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
end
Spinach.hooks.after_scenario do |scenario_data, step_definitions|
......
......@@ -161,6 +161,27 @@ module API
end
end
desc 'Get all references a commit is pushed to' do
detail 'This feature was introduced in GitLab 10.6'
success Entities::BasicRef
end
params do
requires :sha, type: String, desc: 'A commit sha'
optional :type, type: String, values: %w[branch tag all], default: 'all', desc: 'Scope'
use :pagination
end
get ':id/repository/commits/:sha/refs', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit
refs = []
refs.concat(user_project.repository.branch_names_contains(commit.id).map {|name| { type: 'branch', name: name }}) unless params[:type] == 'tag'
refs.concat(user_project.repository.tag_names_contains(commit.id).map {|name| { type: 'tag', name: name }}) unless params[:type] == 'branch'
refs = Kaminari.paginate_array(refs)
present paginate(refs), with: Entities::BasicRef
end
desc 'Post comment to commit' do
success Entities::CommitNote
end
......@@ -170,7 +191,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
requires :line_type, type: String, values: %w[new old], default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
......
......@@ -311,6 +311,10 @@ module API
expose :project_id
end
class BasicRef < Grape::Entity
expose :type, :name
end
class Branch < Grape::Entity
expose :name
......
......@@ -18,11 +18,11 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize(
change, user_access:, project:, skip_authorization: false,
protocol:
skip_lfs_integrity_check: false, protocol:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
......@@ -30,6 +30,7 @@ module Gitlab
@user_access = user_access
@project = project
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
end
......@@ -39,7 +40,7 @@ module Gitlab
push_checks
branch_checks
tag_checks
lfs_objects_exist_check
lfs_objects_exist_check unless skip_lfs_integrity_check
commits_check unless skip_commits_check
true
......
......@@ -253,10 +253,12 @@ module Gitlab
push_size_in_bytes = 0
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
changes_list.each.with_index do |change, index|
first_change = index == 0
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
check_single_change_access(change)
check_single_change_access(change, skip_lfs_integrity_check: !first_change)
if project.size_limit_enabled?
push_size_in_bytes += EE::Gitlab::Deltas.delta_size_check(change, project.repository)
......@@ -268,12 +270,13 @@ module Gitlab
end
end
def check_single_change_access(change)
def check_single_change_access(change, skip_lfs_integrity_check: false)
Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol
).exec
end
......
......@@ -15,7 +15,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
def check_single_change_access(change)
def check_single_change_access(change, _options = {})
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
......
......@@ -117,9 +117,8 @@ module QA
autoload :Show, 'qa/page/project/pipeline/show'
end
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
module Job
autoload :Show, 'qa/page/project/job/show'
end
module Settings
......@@ -170,6 +169,7 @@ module QA
#
module Git
autoload :Repository, 'qa/git/repository'
autoload :Location, 'qa/git/location'
end
##
......
......@@ -4,7 +4,7 @@ module QA
module Factory
module Resource
class Runner < Factory::Base
attr_writer :name, :tags
attr_writer :name, :tags, :image
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-ci-cd'
......@@ -19,6 +19,10 @@ module QA
@tags || %w[qa e2e]
end
def image
@image || 'gitlab/gitlab-runner:alpine'
end
def fabricate!
project.visit!
......@@ -31,6 +35,7 @@ module QA
runner.token = runners.registration_token
runner.address = runners.coordinator_address
runner.tags = tags
runner.image = image
runner.register!
end
end
......
require 'uri'
require 'forwardable'
module QA
module Git
class Location
extend Forwardable
attr_reader :git_uri, :uri
def_delegators :@uri, :user, :host, :path
# See: config/initializers/1_settings.rb
# Settings#build_gitlab_shell_ssh_path_prefix
def initialize(git_uri)
@git_uri = git_uri
@uri =
if git_uri.start_with?('ssh://')
URI.parse(git_uri)
else
*rest, path = git_uri.split(':')
# Host cannot have : so we'll need to escape it
user_host = rest.join('%3A').sub(/\A\[(.+)\]\z/, '\1')
URI.parse("ssh://#{user_host}/#{path}")
end
end
def port
uri.port || 22
end
end
end
end
require 'cgi'
require 'uri'
module QA
......
......@@ -17,7 +17,8 @@ module QA
start = Time.now
while Time.now - start < max
return true if yield
result = yield
return result if result
sleep(time)
......
module QA::Page
module Project::Job
class Show < QA::Page::Base
view 'app/views/projects/jobs/show.html.haml' do
element :build_output, '.js-build-output'
end
def output
css = '.js-build-output'
wait(reload: false) do
has_css?(css)
end
find(css).text
end
end
end
end
......@@ -6,7 +6,13 @@ module QA::Page
end
def go_to_latest_pipeline
first('.js-pipeline-url-link').click
css = '.js-pipeline-url-link'
link = wait(reload: false) do
first(css)
end
link.click
end
end
end
......
......@@ -11,6 +11,7 @@ module QA::Page
view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do
element :job_component, /class.*ci-job-component.*/
element :job_link, /class.*js-pipeline-graph-job-link.*/
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
......@@ -30,6 +31,16 @@ module QA::Page
end
end
end
def go_to_first_job
css = '.js-pipeline-graph-job-link'
wait(reload: false) do
has_css?(css)
end
first(css).click
end
end
end
end
......@@ -22,22 +22,24 @@ module QA
end
def choose_repository_clone_http
wait(reload: false) do
click_element :clone_dropdown
page.within('.clone-options-dropdown') do
click_link('HTTP')
end
choose_repository_clone('HTTP', 'http')
end
# Ensure git clone textbox was updated to http URI
repository_location.include?('http')
end
def choose_repository_clone_ssh
# It's not always beginning with ssh:// so detecting with @
# would be more reliable because ssh would always contain it.
# We can't use .git because HTTP also contain that part.
choose_repository_clone('SSH', '@')
end
def repository_location
find('#project_clone').value
end
def repository_location_uri
Git::Location.new(repository_location)
end
def project_name
find('.qa-project-name').text
end
......@@ -56,6 +58,21 @@ module QA
click_link 'New issue'
end
private
def choose_repository_clone(kind, detect_text)
wait(reload: false) do
click_element :clone_dropdown
page.within('.clone-options-dropdown') do
click_link(kind)
end
# Ensure git clone textbox was updated
repository_location.include?(detect_text)
end
end
end
end
end
......
......@@ -7,7 +7,7 @@ module QA
extend Forwardable
attr_reader :key
def_delegators :@key, :fingerprint
def_delegators :@key, :fingerprint, :to_pem
def initialize(bits = 4096)
@key = OpenSSL::PKey::RSA.new(bits)
......
require 'digest/sha1'
module QA
feature 'cloning code using a deploy key', :core, :docker do
let(:runner_name) { "qa-runner-#{Time.now.to_i}" }
let(:key) { Runtime::RSAKey.new }
given(:project) do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
end
after do
Service::Runner.new(runner_name).remove!
end
scenario 'user sets up a deploy key to clone code using pipelines' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Runner.fabricate! do |resource|
resource.project = project
resource.name = runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
Factory::Resource::DeployKey.fabricate! do |resource|
resource.project = project
resource.title = 'deploy key title'
resource.key = key.public_key
end
Factory::Resource::SecretVariable.fabricate! do |resource|
resource.project = project
resource.key = 'DEPLOY_KEY'
resource.value = key.to_pem
end
project.visit!
repository_uri = Page::Project::Show.act do
choose_repository_clone_ssh
repository_location_uri
end
gitlab_ci = <<~YAML
cat-config:
script:
- mkdir -p ~/.ssh
- ssh-keyscan -p #{repository_uri.port} #{repository_uri.host} >> ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- echo "$DEPLOY_KEY" | ssh-add -
- git clone #{repository_uri.git_uri}
- sha1sum #{project.name}/.gitlab-ci.yml
tags:
- qa
- docker
YAML
Factory::Repository::Push.fabricate! do |resource|
resource.project = project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
resource.file_content = gitlab_ci
end
sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
Page::Project::Show.act { wait_for_push }
Page::Menu::Side.act { click_ci_cd_pipelines }
Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
Page::Project::Pipeline::Show.act { go_to_first_job }
Page::Project::Job::Show.perform do |job|
expect(job.output).to include(sha1sum)
end
end
end
end
describe QA::Git::Location do
describe '.new' do
context 'when URI starts with ssh://' do
context 'when URI has port' do
it 'parses correctly' do
uri = described_class
.new('ssh://git@qa.test:2222/sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(2222)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
context 'when URI does not have port' do
it 'parses correctly' do
uri = described_class
.new('ssh://git@qa.test/sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
end
context 'when URI does not start with ssh://' do
context 'when host does not have colons' do
it 'parses correctly' do
uri = described_class
.new('git@qa.test:sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
context 'when host has a colon' do
it 'parses correctly' do
uri = described_class
.new('[git@qa:test]:sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa%3Atest')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
end
end
end
#!/usr/bin/env ruby
require 'digest'
require 'fileutils'
harness_path = File.expand_path('../.git/security_harness', __dir__)
hook_path = File.expand_path("../.git/hooks/pre-push", __dir__)
if File.exist?(hook_path)
# Deal with a pre-existing hook
source_sum = Digest::SHA256.hexdigest(DATA.read)
dest_sum = Digest::SHA256.file(hook_path).hexdigest
if source_sum != dest_sum
puts "#{hook_path} exists and is different from our hook!"
puts "Remove it and re-run this script to continue."
exit 1
end
else
File.open(hook_path, 'w') do |file|
IO.copy_stream(DATA, file)
end
end
# Toggle the harness on or off
if File.exist?(harness_path)
FileUtils.rm(harness_path)
puts "Security harness removed -- you can now push to all remotes."
else
FileUtils.touch(harness_path)
puts "Security harness installed -- you will only be able to push to dev.gitlab.org!"
end
__END__
#!/bin/sh
set -e
url="$2"
harness=`dirname "$0"`/../security_harness
if [ -e "$harness" ]
then
if [[ "$url" != *"dev.gitlab.org"* ]]
then
echo "Pushing to remotes other than dev.gitlab.org has been disabled!"
echo "Run scripts/security-harness to disable this check."
echo
exit 1
fi
fi
require 'spec_helper'
describe Projects::VariablesController do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
sign_in(user)
project.add_master(user)
allow_any_instance_of(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:variable_environment_scope).and_return(true)
end
describe 'PATCH #update' do
let!(:variable) { create(:ci_variable, project: project, environment_scope: 'custom_scope') }
let(:owner) { project }
let(:variable_attributes) do
{ id: variable.id,
key: variable.key,
value: variable.value,
protected: variable.protected?.to_s,
environment_scope: variable.environment_scope }
end
let(:new_variable_attributes) do
{ key: 'new_key',
value: 'dummy_value',
protected: 'false',
environment_scope: 'new_scope' }
end
subject do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
variables_attributes: variables_attributes,
format: :json
end
context 'with same key and different environment scope' do
let(:variables_attributes) do
[
variable_attributes,
new_variable_attributes.merge(key: variable.key)
]
end
it 'does not update the existing variable' do
expect { subject }.not_to change { variable.reload.value }
end
it 'creates the new variable' do
expect { subject }.to change { owner.variables.count }.by(1)
end
it 'returns a successful response' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'has all variables in response' do
subject
expect(response).to match_response_schema('variables')
end
end
context 'with same key and same environment scope' do
let(:variables_attributes) do
[
variable_attributes,
new_variable_attributes.merge(key: variable.key, environment_scope: variable.environment_scope)
]
end
it 'does not update the existing variable' do
expect { subject }.not_to change { variable.reload.value }
end
it 'does not create the new variable' do
expect { subject }.not_to change { owner.variables.count }
end
it 'returns a bad request response' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
require 'spec_helper'
describe 'User activates issue tracker', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:url) { 'http://tracker.example.com' }
def fill_form(active = true)
check 'Active' if active
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"
fill_in 'service_new_issue_url', with: url
end
before do
project.add_master(user)
sign_in(user)
visit project_settings_integrations_path(project)
end
shared_examples 'external issue tracker activation' do |tracker:|
describe 'user sets and activates the Service' do
context 'when the connection test succeeds' do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
click_link(tracker)
fill_form
click_button('Test settings and save changes')
wait_for_requests
end
it 'activates the service' do
expect(page).to have_content("#{tracker} activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'shows the link in the menu' do
page.within('.nav-sidebar') do
expect(page).to have_link(tracker, href: url)
end
end
end
context 'when the connection test fails' do
it 'activates the service' do
stub_request(:head, url).to_raise(HTTParty::Error)
click_link(tracker)
fill_form
click_button('Test settings and save changes')
wait_for_requests
expect(find('.flash-container-page')).to have_content 'Test failed.'
expect(find('.flash-container-page')).to have_content 'Save anyway'
find('.flash-alert .flash-action').click
wait_for_requests
expect(page).to have_content("#{tracker} activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
end
end
describe 'user sets the service but keeps it disabled' do
before do
click_link(tracker)
fill_form(false)
click_button('Save changes')
end
it 'saves but does not activate the service' do
expect(page).to have_content("#{tracker} settings saved, but not activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'does not show the external tracker link in the menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link(tracker, href: url)
end
end
end
end
it_behaves_like 'external issue tracker activation', tracker: 'Redmine'
it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla'
it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker'
end
......@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'User activates Jira', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:service) { project.create_jira_service }
let(:url) { 'http://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
......@@ -26,7 +25,7 @@ describe 'User activates Jira', :js do
describe 'user sets and activates Jira Service' do
context 'when Jira connection test succeeds' do
it 'activates the JIRA service' do
before do
server_info = { key: 'value' }.to_json
WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
......@@ -34,10 +33,18 @@ describe 'User activates Jira', :js do
fill_form
click_button('Test settings and save changes')
wait_for_requests
end
it 'activates the JIRA service' do
expect(page).to have_content('JIRA activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'shows the JIRA link in the menu' do
page.within('.nav-sidebar') do
expect(page).to have_link('JIRA', href: url)
end
end
end
context 'when Jira connection test fails' do
......@@ -75,14 +82,20 @@ describe 'User activates Jira', :js do
end
describe 'user sets Jira Service but keeps it disabled' do
context 'when Jira connection test succeeds' do
it 'activates the JIRA service' do
click_link('JIRA')
fill_form(false)
click_button('Save changes')
before do
click_link('JIRA')
fill_form(false)
click_button('Save changes')
end
expect(page).to have_content('JIRA settings saved, but not activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
it 'saves but does not activate the JIRA service' do
expect(page).to have_content('JIRA settings saved, but not activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'does not show the JIRA link in the menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link('JIRA', href: url)
end
end
end
......
......@@ -10,7 +10,8 @@
"id": { "type": "integer" },
"key": { "type": "string" },
"value": { "type": "string" },
"protected": { "type": "boolean" }
"protected": { "type": "boolean" },
"environment_scope": { "type": "string", "optional": true }
},
"additionalProperties": false
}
......@@ -63,13 +63,29 @@ describe ApplicationHelper do
end
end
describe 'avatar_icon' do
describe 'avatar_icon_for' do
let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
let(:email) { 'foo@example.com' }
let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
it 'prefers the user to retrieve the avatar_url' do
expect(helper.avatar_icon_for(user, email).to_s)
.to eq(user.avatar.url)
end
it 'falls back to email lookup if no user given' do
expect(helper.avatar_icon_for(nil, email).to_s)
.to eq(another_user.avatar.url)
end
end
describe 'avatar_icon_for_email' do
let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
context 'using an email' do
context 'when there is a matching user' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user.email).to_s)
expect(helper.avatar_icon_for_email(user.email).to_s)
.to eq(user.avatar.url)
end
end
......@@ -78,17 +94,37 @@ describe ApplicationHelper do
it 'calls gravatar_icon' do
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
helper.avatar_icon('foo@example.com', 20, 2)
helper.avatar_icon_for_email('foo@example.com', 20, 2)
end
end
context 'without an email passed' do
it 'calls gravatar_icon' do
expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
helper.avatar_icon_for_email(nil, 20, 2)
end
end
end
end
describe 'avatar_icon_for_user' do
let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
describe 'using a user' do
context 'with a user object passed' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user).to_s)
expect(helper.avatar_icon_for_user(user).to_s)
.to eq(user.avatar.url)
end
end
context 'without a user object passed' do
it 'calls gravatar_icon' do
expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
helper.avatar_icon_for_user(nil, 20, 2)
end
end
end
describe 'gravatar_icon' do
......
......@@ -29,7 +29,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, 16),
src: avatar_icon_for_user(user, 16),
data: { container: 'body' },
class: 'avatar s16 has-tooltip',
title: user.name
......@@ -43,7 +43,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, 16),
src: avatar_icon_for_user(user, 16),
data: { container: 'body' },
class: "avatar s16 #{options[:css_class]} has-tooltip",
title: user.name
......@@ -58,7 +58,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, options[:size]),
src: avatar_icon_for_user(user, options[:size]),
data: { container: 'body' },
class: "avatar s#{options[:size]} has-tooltip",
title: user.name
......@@ -89,7 +89,7 @@ describe AvatarsHelper do
:img,
alt: "#{user.name}'s avatar",
src: LazyImageTagHelper.placeholder_image,
data: { container: 'body', src: avatar_icon(user, 16) },
data: { container: 'body', src: avatar_icon_for_user(user, 16) },
class: "avatar s16 has-tooltip lazy",
title: user.name
)
......@@ -104,7 +104,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, 16),
src: avatar_icon_for_user(user, 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
......@@ -119,7 +119,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, 16),
src: avatar_icon_for_user(user, 16),
class: "avatar s16",
title: user.name
)
......@@ -137,7 +137,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: avatar_icon(user, 16),
src: avatar_icon_for_user(user, 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
......@@ -149,7 +149,7 @@ describe AvatarsHelper do
is_expected.to eq tag(
:img,
alt: "#{options[:user_name]}'s avatar",
src: avatar_icon(options[:user_email], 16),
src: avatar_icon_for_email(options[:user_email], 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: options[:user_name]
......
......@@ -215,7 +215,7 @@ describe ProjectsHelper do
let(:expected) { double }
before do
expect(helper).to receive(:avatar_icon).with(user, 16).and_return(expected)
expect(helper).to receive(:avatar_icon_for_user).with(user, 16).and_return(expected)
end
it 'returns image tag for member avatar' do
......
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
Vue.use(Translate);
......
import Vue from 'vue';
import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
......
import Vue from 'vue';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetNotAllowed', () => {
describe('template', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(notAllowedComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request');
});
vm = mountComponent(Component);
});
afterEach(() => {
vm.$destroy();
});
it('renders success icon', () => {
expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null);
});
it('renders informative text', () => {
expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request');
});
});
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetPipelineBlocked', () => {
describe('template', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(pipelineBlockedComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
vm = mountComponent(Component);
});
afterEach(() => {
vm.$destroy();
});
it('renders warning icon', () => {
expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null);
});
it('renders information text', () => {
expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
require 'spec_helper'
describe Gitlab::Email::Handler do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
describe '.for' do
it 'picks issue handler if there is not merge request prefix' do
expect(described_class.for('email', 'project+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
......@@ -18,43 +13,5 @@ describe Gitlab::Email::Handler do
it 'returns nil if no handler is found' do
expect(described_class.for('email', '')).to be_nil
end
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end
it 'uses no handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_nil
end
end
context 'a new issue email' do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
end
......@@ -18,8 +18,9 @@ describe Gitlab::GitAccess do
redirected_path: redirected_path)
end
let(:push_access_check) { access.check('git-receive-pack', '_any') }
let(:pull_access_check) { access.check('git-upload-pack', '_any') }
let(:changes) { '_any' }
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
......@@ -646,6 +647,20 @@ describe Gitlab::GitAccess do
end
end
describe 'check LFS integrity' do
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] }
before do
project.add_developer(user)
end
it 'checks LFS integrity only for first change' do
expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times
push_access_check
end
end
describe '#check_push_access!' do
let(:unprotected_branch) { 'unprotected_branch' }
......
class IssuesCsvMailerPreview < ActionMailer::Preview
def issues_csv_export
user = OpenStruct.new(notification_email: 'a@example.com')
project = Project.unscoped.first
Notify.issues_csv_email(user, project, "Dummy,Csv\n0,1", export_status)
end
private
def export_status
{
truncated: [true, false].sample,
rows_written: 632,
rows_expected: 891
}
end
end
......@@ -473,6 +473,72 @@ describe API::Commits do
end
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
let(:project) { create(:project, :public, :repository) }
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
context 'when ref does not exist' do
let(:commit_id) { 'unknown' }
it_behaves_like '404 response' do
let(:request) { get api(route, current_user) }
let(:message) { '404 Commit Not Found' }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user) }
end
end
context 'for a valid commit' do
it 'returns all refs with no scope' do
get api(route, current_user), per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]})
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns all refs' do
get api(route, current_user), type: 'all', per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]})
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns the branch refs' do
get api(route, current_user), type: 'branch', per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns the tag refs' do
get api(route, current_user), type: 'tag', per_page: 100
refs = project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
end
end
describe 'GET /projects/:id/repository/commits/:sha' do
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
......
......@@ -21,6 +21,15 @@ describe Members::AuthorizedDestroyService do
.to change { Member.count }.from(3).to(2)
end
it "doesn't destroy invited project member notification_settings" do
project.add_developer(member_user)
member = create :project_member, :invited, project: project
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
it 'destroys invited group member' do
group.add_developer(member_user)
......@@ -29,38 +38,73 @@ describe Members::AuthorizedDestroyService do
expect { described_class.new(member, member_user).execute }
.to change { Member.count }.from(2).to(1)
end
it "doesn't destroy invited group member notification_settings" do
group.add_developer(member_user)
member = create :group_member, :invited, group: group
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Requested user' do
it "doesn't destroy member notification_settings" do
member = create(:project_member, user: member_user, requested_at: Time.now)
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Group member' do
it "unassigns issues and merge requests" do
let(:member) { group.members.find_by(user_id: member_user.id) }
before do
group.add_developer(member_user)
end
it "unassigns issues and merge requests" do
issue = create :issue, project: group_project, assignees: [member_user]
create :issue, assignees: [member_user]
merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
create :merge_request, target_project: project, source_project: project, assignee: member_user
member = group.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
it 'destroys member notification_settings' do
group.add_developer(member_user)
member = group.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
context 'Project member' do
it "unassigns issues and merge requests" do
let(:member) { project.members.find_by(user_id: member_user.id) }
before do
project.add_developer(member_user)
end
it "unassigns issues and merge requests" do
create :issue, project: project, assignees: [member_user]
create :merge_request, target_project: project, source_project: project, assignee: member_user
member = project.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(2).to(0)
end
it 'destroys member notification_settings' do
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
end
......@@ -263,7 +263,7 @@ shared_examples 'variable list' do
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section') do
expect(find('.js-ci-variable-error-box')).to have_content('Validation failed Variables Duplicate variables: samekey')
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
end
end
end
module FixtureHelpers
def fixture_file(filename)
def fixture_file(filename, dir: '')
return '' if filename.blank?
File.read(expand_fixture_path(filename))
File.read(expand_fixture_path(filename, dir: dir))
end
def fixture_file_ee(filename)
return '' if filename.blank?
File.read(expand_fixture_ee_path(filename))
end
def expand_fixture_path(filename)
File.expand_path(Rails.root.join('spec/fixtures/', filename))
end
def expand_fixture_ee_path(filename)
File.expand_path(Rails.root.join('ee/spec/fixtures/', filename))
def expand_fixture_path(filename, dir: '')
File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end
end
......
require_dependency Rails.root.join('ee', 'spec', 'support', 'stub_configuration')
require_dependency Rails.root.join('ee', 'spec', 'support', 'ee', 'stub_configuration')
module StubConfiguration
prepend EE::StubConfiguration
......
require 'spec_helper'
describe VariableDuplicatesValidator do
let(:validator) { described_class.new(attributes: [:variables], **options) }
describe '#validate_each' do
let(:project) { build(:project) }
subject { validator.validate_each(project, :variables, project.variables) }
context 'with no scope' do
let(:options) { {} }
let(:variables) { build_list(:ci_variable, 2, project: project) }
before do
project.variables << variables
end
it 'does not have any errors' do
subject
expect(project.errors.empty?).to be true
end
context 'with duplicates' do
before do
project.variables.build(key: variables.first.key, value: 'dummy_value')
end
it 'has a duplicate key error' do
subject
expect(project.errors).to have_key(:variables)
end
end
end
context 'with a scope attribute' do
let(:options) { { scope: :environment_scope } }
let(:first_variable) { build(:ci_variable, key: 'test_key', environment_scope: '*', project: project) }
let(:second_variable) { build(:ci_variable, key: 'test_key', environment_scope: 'prod', project: project) }
before do
project.variables << first_variable
project.variables << second_variable
end
it 'does not have any errors' do
subject
expect(project.errors.empty?).to be true
end
context 'with duplicates' do
before do
project.variables.build(key: second_variable.key, value: 'dummy_value', environment_scope: second_variable.environment_scope)
end
it 'has a duplicate key error' do
subject
expect(project.errors).to have_key(:variables)
end
end
end
end
end
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