Commit fc42ba6c authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into 44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui

parents d3327e0d 75f6e91f
......@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.7.2 (2018-04-25)
### Security (2 changes)
- Serve archive requests with the correct file in all cases.
- Sanitizes user name to avoid XSS attacks.
## 10.7.1 (2018-04-23)
### Fixed (11 changes)
......@@ -237,6 +245,13 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.5 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
......@@ -478,6 +493,13 @@ entry.
- Use host URL to build JIRA remote link icon.
## 10.5.8 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.5.7 (2018-04-03)
### Security (2 changes)
......
......@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
# User agent parsing
gem 'device_detector'
# Cache
gem 'redis-rails', '~> 5.0.2'
......
......@@ -161,6 +161,7 @@ GEM
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
......@@ -1026,6 +1027,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
device_detector
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
......
......@@ -304,12 +304,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.1)
gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
......@@ -602,8 +602,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
......@@ -752,7 +750,7 @@ GEM
retriable (3.1.1)
rinku (2.0.4)
rotp (2.1.2)
rouge (2.2.1)
rouge (3.1.1)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7)
......@@ -1134,7 +1132,6 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
......@@ -1166,7 +1163,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
......
......@@ -86,7 +86,7 @@ export default {
v-html="resolveSvg"
></span>
</span>
<span class=".line-resolve-text">
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span>
</div>
......
<script>
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
......@@ -20,12 +19,6 @@ export default {
store: new Store(),
};
},
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
......@@ -42,6 +35,7 @@ export default {
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/>
</div>
</template>
......@@ -3,7 +3,6 @@
import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
......@@ -48,7 +47,7 @@
},
methods: {
toggleSubscription() {
eventHub.$emit('toggleSubscription', this.id);
this.$emit('toggleSubscription', this.id);
},
},
};
......
......@@ -17,7 +17,7 @@ export default {
},
computed: {
/**
* This method is based on app/helpers/application_helper.rb#project_identicon
* This method is based on app/helpers/avatars_helper.rb#project_identicon
*/
identiconStyles() {
const allowedColors = [
......
......@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
......
......@@ -39,35 +39,10 @@
svg {
fill: currentColor;
&.s8 {
@include svg-size(8px);
}
&.s12 {
@include svg-size(12px);
}
&.s16 {
@include svg-size(16px);
}
&.s18 {
@include svg-size(18px);
}
&.s24 {
@include svg-size(24px);
}
&.s32 {
@include svg-size(32px);
}
&.s48 {
@include svg-size(48px);
}
&.s72 {
@include svg-size(72px);
$svg-sizes: 8 12 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
}
}
}
......@@ -107,6 +107,16 @@
padding-top: 10px;
}
.referenced-commands {
background: $blue-50;
padding: $gl-padding-8 $gl-padding;
border-radius: $border-radius-default;
p {
margin: 0;
}
}
.md-preview-holder {
min-height: 167px;
padding: 10px 0;
......
......@@ -772,7 +772,3 @@ ul.notes {
height: auto;
}
}
.line-resolve-text {
vertical-align: middle;
}
......@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
......
......@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end
out_of_range
......
......@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
url_for(params)
url_for(safe_params)
end
end
......@@ -8,8 +8,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
omniauth_flow(Gitlab::Auth::OAuth)
end
Gitlab.config.omniauth.providers.each do |provider|
alias_method provider['name'], :handle_omniauth
AuthHelper.providers_for_base_controller.each do |provider|
alias_method provider, :handle_omniauth
end
# Extend the standard implementation to also increment
......
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
@sessions = ActiveSession.list(current_user)
end
def destroy
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: 302 }
format.js { head :ok }
end
end
end
......@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param
url_for(params)
url_for(safe_params)
end
def repository
......
......@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project
object.save!
object.lfs_objects_projects.create!(project: storage_project)
end
end
end
......@@ -33,9 +33,7 @@ class Projects::NotesController < Projects::ApplicationController
def resolve
return render_404 unless note.resolvable?
note.resolve!(current_user)
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
Notes::ResolveService.new(project, current_user).execute(note)
discussion = note.discussion
......
......@@ -39,7 +39,7 @@ class GroupsFinder < UnionFinder
def all_groups
return [owned_groups] if params[:owned]
return [Group.all] if current_user&.full_private_access?
return [Group.all] if current_user&.full_private_access? && all_available?
groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
......@@ -67,6 +67,10 @@ class GroupsFinder < UnionFinder
end
def include_public_groups?
current_user.nil? || params.fetch(:all_available, true)
current_user.nil? || all_available?
end
def all_available?
params.fetch(:all_available, true)
end
end
module ActiveSessionsHelper
# Maps a device type as defined in `ActiveSession` to an svg icon name and
# outputs the icon html.
#
# see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
def active_session_device_type_icon(active_session)
icon_name =
case active_session.device_type
when 'smartphone', 'feature phone', 'phablet'
'mobile'
when 'tablet'
'tablet'
when 'tv', 'smart display', 'camera', 'portable media player', 'console'
'media'
when 'car browser'
'car'
else
'monitor-o'
end
sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
end
end
......@@ -32,80 +32,6 @@ module ApplicationHelper
args.any? { |v| v.to_s.downcase == action_name }
end
def project_icon(project_id, options = {})
project =
if project_id.respond_to?(:avatar_url)
project_id
else
Project.find_by_full_path(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
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(nil, size, scale)
end
end
def gravatar_icon(user_email = '', size = nil, scale = 2)
GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
def default_avatar
asset_path('no_avatar.png')
end
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
......
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
LDAP_PROVIDER = /\Aldap/
def ldap_enabled?
Gitlab::Auth::LDAP::Config.enabled?
......@@ -23,7 +23,7 @@ module AuthHelper
end
def form_based_provider?(name)
FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
[LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end
def form_based_providers
......@@ -38,6 +38,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
def providers_for_base_controller
auth_providers.reject { |provider| LDAP_PROVIDER === provider }
end
def enabled_button_based_providers
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
......
module AvatarsHelper
def project_icon(project_id, options = {})
project =
if project_id.respond_to?(:avatar_url)
project_id
else
Project.find_by_full_path(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
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(nil, size, scale)
end
end
def gravatar_icon(user_email = '', size = nil, scale = 2)
GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
def default_avatar
ActionController::Base.helpers.image_path('no_avatar.png')
end
def author_avatar(commit_or_event, options = {})
user_avatar(options.merge({
user: commit_or_event.author,
......
......@@ -442,7 +442,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
......
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'commit',
'description' => 'pencil',
'description' => 'pencil-square',
'merge' => 'git-merge',
'merged' => 'git-merge',
'opened' => 'issue-open',
'closed' => 'issue-close',
'time_tracking' => 'timer',
'assignee' => 'user',
'title' => 'pencil',
'title' => 'pencil-square',
'task' => 'task-done',
'label' => 'label',
'cross_reference' => 'comment-dots',
......@@ -18,7 +18,7 @@ module SystemNoteHelper
'milestone' => 'clock',
'discussion' => 'comment',
'moved' => 'arrow-right',
'outdated' => 'pencil',
'outdated' => 'pencil-square',
'duplicate' => 'issue-duplicate',
'locked' => 'lock',
'unlocked' => 'lock-open'
......
......@@ -16,6 +16,7 @@ class Notify < BaseMailer
helper BlobHelper
helper EmailsHelper
helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
......
class ActiveSession
include ActiveModel::Model
attr_accessor :created_at, :updated_at,
:session_id, :ip_address,
:browser, :os, :device_name, :device_type
def current?(session)
return false if session_id.nil? || session.id.nil?
session_id == session.id
end
def human_device_type
device_type&.titleize
end
def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
session_id = request.session.id
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
active_user_session = new(
ip_address: request.ip,
browser: client.name,
os: client.os_name,
device_name: client.device_name,
device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp,
session_id: session_id
)
redis.pipelined do
redis.setex(
key_name(user.id, session_id),
Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session)
)
redis.sadd(
lookup_key_name(user.id),
session_id
)
end
end
end
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id).map do |entry|
# rubocop:disable Security/MarshalLoad
Marshal.load(entry)
# rubocop:enable Security/MarshalLoad
end
end
end
def self.destroy(user, session_id)
Gitlab::Redis::SharedState.with do |redis|
redis.srem(lookup_key_name(user.id), session_id)
deleted_keys = redis.del(key_name(user.id, session_id))
# only allow deleting the devise session if we could actually find a
# related active session. this prevents another user from deleting
# someone else's session.
if deleted_keys > 0
redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
end
end
end
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id)
end
end
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
def self.lookup_key_name(user_id)
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
def self.cleaned_up_lookup_entries(redis, user_id)
lookup_key = lookup_key_name(user_id)
session_ids = redis.smembers(lookup_key)
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
return [] if entry_keys.empty?
entries = redis.mget(entry_keys)
session_ids_and_entries = session_ids.zip(entries)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
redis.srem(lookup_key, session_id)
end
session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
end
end
......@@ -13,7 +13,7 @@ module Ci
after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
after_save :update_file_store
after_save :update_file_store, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
......
......@@ -532,6 +532,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# These methods overwrite autogenerated ones to return correct results.
def unknown?
Gitlab.rails5? ? source.nil? : super
end
def unknown_source?
Gitlab.rails5? ? config_source.nil? : super
end
private
def ci_yaml_from_repo
......
......@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
validates :project, presence: true, unless: :importing?
validates :pipeline, presence: true, unless: :importing?
validates :name, presence: true, unless: :importing?
with_options unless: :importing? do
validates :project, presence: true
validates :pipeline, presence: true
validates :name, presence: true
validates :position, presence: true
end
after_initialize do |stage|
after_initialize do
self.status = DEFAULT_STATUS if self.status.nil?
end
before_validation unless: :importing? do
next if position.present?
self.position = statuses.select(:stage_idx)
.where('stage_idx IS NOT NULL')
.group(:stage_idx)
.order('COUNT(*) DESC')
.first&.stage_idx.to_i
end
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
......
......@@ -105,6 +105,10 @@ class Commit
end
end
end
def parent_class
::Project
end
end
attr_accessor :raw
......@@ -420,6 +424,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined
end
def touch_later
# No-op.
# This method is called by ActiveRecord.
# We don't want to do anything for `Commit` model, so this is empty.
end
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress?
......
......@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# This method overwrites the autogenerated one to return correct result.
def unknown_failure?
Gitlab.rails5? ? failure_reason.nil? : super
end
end
......@@ -54,7 +54,20 @@ class DiffNote < Note
end
def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository)
@diff_file ||=
begin
if created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it.
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
else
original_position.diff_file(self.project.repository)
end
end
end
def diff_line
......
......@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
after_save :update_file_store
after_save :update_file_store, if: :file_changed?
def update_file_store
# The file.object_store is set during `uploader.store!`
......
......@@ -910,7 +910,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end
def notification_service
......
......@@ -42,6 +42,7 @@ module Ci
def create_stage
Ci::Stage.create!(name: @build.stage,
position: @build.stage_idx,
pipeline: @build.pipeline,
project: @build.project)
end
......
module Notes
class ResolveService < ::BaseService
def execute(note)
note.resolve!(current_user)
::MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
end
end
end
- if current_user.admin?
.form-group
= f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
.col-sm-10
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @group.lfs_enabled?
%strong
Allow projects within this group to use Git LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
%span.descr This setting can be overridden in each project.
.form-group
= f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
.col-sm-10
.checkbox
= f.label :lfs_enabled do
= f.check_box :lfs_enabled, checked: @group.lfs_enabled?
%strong
Allow projects within this group to use Git LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
%span.descr This setting can be overridden in each project.
- if can? current_user, :admin_group, @group
.form-group
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
%strong
Require all users in this group to setup Two-factor authentication
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.text_field :two_factor_grace_period, class: 'form-control'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
%strong
Require all users in this group to setup Two-factor authentication
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.text_field :two_factor_grace_period, class: 'form-control'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
......@@ -129,6 +129,17 @@
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
#{ _('Preferences') }
= nav_link(controller: :active_sessions) do
= link_to profile_active_sessions_path do
.nav-icon-container
= sprite_icon('monitor-lines')
%span.nav-item-name
Active Sessions
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_active_sessions_path do
%strong.fly-out-top-item-name
#{ _('Active Sessions') }
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do
.nav-icon-container
......
......@@ -3,5 +3,5 @@
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
peek_url: peek_routes.results_url,
profile_url: url_for(params.merge(lineprofiler: 'true')) },
profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env }
- is_current_session = active_session.current?(session)
%li
.pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
= active_session_device_type_icon(active_session)
.description.pull-left
%div
%strong= active_session.ip_address
- if is_current_session
%div This is your current session
- else
%div
Last accessed on
= l(active_session.updated_at, format: :short)
%div
%strong= active_session.browser
on
%strong= active_session.os
%div
%strong Signed in
on
= l(active_session.created_at, format: :short)
- unless is_current_session
.pull-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
- page_title 'Active Sessions'
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
.col-lg-8
.append-bottom-default
%ul.well-list
= render partial: 'profiles/active_sessions/active_session', collection: @sessions
......@@ -8,18 +8,17 @@
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name' do
= sprite_icon('fork', size: 12)
= sprite_icon('fork', size: 12)
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
%span.label.label-primary.prepend-left-5 default
- elsif merged
%span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
%span.label.label-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
- if protected_branch?(@project, branch)
%span.label.label-success
%span.label.label-success.prepend-left-5
= s_('Branches|protected')
.block-truncated
......
......@@ -8,7 +8,7 @@
.files-changed-inner
.inline-parallel-buttons.hidden-xs.hidden-sm
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
= link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
......
---
title: Replace the `project/source/markdown_render.feature` spinach test with an rspec analog
merge_request: 18525
author: "@blackst0ne"
type: other
---
title: Fix commit trailer rendering when Gravatar is disabled
merge_request:
author:
type: fixed
---
title: Display active sessions and allow the user to revoke any of it
merge_request: 17867
author: Alexis Reigel
type: added
---
title: For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors)
merge_request: 17884
author: Roger Rüttimann
type: changed
---
title: Fix file_store for artifacts and lfs when saving
merge_request:
author:
type: fixed
---
title: Fixed inconsistent protected branch pill baseline
merge_request:
author:
type: fixed
---
title: Increase cluster applications installer availability using alpine linux mirrors
merge_request:
author:
type: performance
---
title: Improve quick actions summary preview
merge_request: 18659
author: George Tsiolis
type: changed
---
title: Add discussion API for merge requests and commits
merge_request:
author:
type: added
---
title: Show group and project LFS settings in the interface to Owners and Masters
merge_request: 18562
author:
type: changed
---
title: Use persisted diff data instead fetching Git on discussions
merge_request:
author:
type: performance
---
title: Revert discussion counter height
merge_request: 18656
author: George Tsiolis
type: changed
---
title: Update timeline icon for description edit
merge_request: 18633
author: George Tsiolis
type: changed
......@@ -15,19 +15,15 @@ cookie_key = if Rails.env.development?
"_gitlab_session"
end
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
sessions_config = Gitlab::Redis::SharedState.params
sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
sessions_config = Gitlab::Redis::SharedState.params
sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: sessions_config,
key: cookie_key,
secure: Gitlab.config.gitlab.https,
httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60,
path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
)
end
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: sessions_config,
key: cookie_key,
secure: Gitlab.config.gitlab.https,
httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60,
path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
)
......@@ -6,4 +6,16 @@ Rails.application.configure do |config|
Warden::Manager.before_failure do |env, opts|
Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
end
Warden::Manager.after_authentication do |user, auth, opts|
ActiveSession.cleanup(user)
end
Warden::Manager.after_set_user only: :fetch do |user, auth, opts|
ActiveSession.set(user, auth.request)
end
Warden::Manager.before_logout do |user, auth, opts|
ActiveSession.destroy(user || auth.user, auth.request.session.id)
end
end
var path = require('path');
var webpack = require('webpack');
var argumentsParser = require('commander');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
const path = require('path');
const glob = require('glob');
const chalk = require('chalk');
const webpack = require('webpack');
const argumentsParser = require('commander');
const webpackConfig = require('./webpack.config.js');
const ROOT_PATH = path.resolve(__dirname, '..');
function fatalError(message) {
console.error(chalk.red(`\nError: ${message}\n`));
process.exit(1);
}
// remove problematic plugins
if (webpackConfig.plugins) {
......@@ -15,33 +23,70 @@ if (webpackConfig.plugins) {
});
}
var testFiles = argumentsParser
const specFilters = argumentsParser
.option(
'-f, --filter-spec [filter]',
'Filter run spec files by path. Multiple filters are like a logical OR.',
(val, memo) => {
memo.push(val);
(filter, memo) => {
memo.push(filter, filter.replace(/\/?$/, '/**/*.js'));
return memo;
},
[]
)
.parse(process.argv).filterSpec;
webpackConfig.plugins.push(
new webpack.DefinePlugin({
'process.env.TEST_FILES': JSON.stringify(testFiles),
})
);
if (specFilters.length) {
const specsPath = /^(?:\.[\\\/])?spec[\\\/]javascripts[\\\/]/;
// resolve filters
let filteredSpecFiles = specFilters.map(filter =>
glob
.sync(filter, {
root: ROOT_PATH,
matchBase: true,
})
.filter(path => path.endsWith('spec.js'))
);
// flatten
filteredSpecFiles = Array.prototype.concat.apply([], filteredSpecFiles);
// remove duplicates
filteredSpecFiles = [...new Set(filteredSpecFiles)];
if (filteredSpecFiles.length < 1) {
fatalError('Your filter did not match any test files.');
}
if (!filteredSpecFiles.every(file => specsPath.test(file))) {
fatalError('Test files must be located within /spec/javascripts.');
}
const newContext = filteredSpecFiles.reduce((context, file) => {
const relativePath = file.replace(specsPath, '');
context[file] = `./${relativePath}`;
return context;
}, {});
webpackConfig.plugins.push(
new webpack.ContextReplacementPlugin(
/spec[\\\/]javascripts$/,
path.join(ROOT_PATH, 'spec/javascripts'),
newContext
)
);
}
webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map';
webpackConfig.entry = undefined;
webpackConfig.devtool = 'cheap-inline-source-map';
// Karma configuration
module.exports = function(config) {
process.env.TZ = 'Etc/UTC';
var progressReporter = process.env.CI ? 'mocha' : 'progress';
const progressReporter = process.env.CI ? 'mocha' : 'progress';
var karmaConfig = {
const karmaConfig = {
basePath: ROOT_PATH,
browsers: ['ChromeHeadlessCustom'],
customLaunchers: {
......
......@@ -30,6 +30,7 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
resources :active_sessions, only: [:index, :destroy]
resources :emails, only: [:index, :create, :destroy] do
member do
put :resend_confirmation_instructions
......
......@@ -69,6 +69,9 @@ const config = {
test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader',
options: {
cacheDirectory: path.join(ROOT_PATH, 'tmp/cache/babel-loader'),
},
},
{
test: /\.vue$/,
......
class AddTmpStagePriorityIndexToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:ci_builds, [:stage_id, :stage_idx],
where: 'stage_idx IS NOT NULL', name: 'tmp_build_stage_position_index')
end
def down
remove_concurrent_index_by_name(:ci_builds, 'tmp_build_stage_position_index')
end
end
class AddIndexToCiStage < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_stages, :position, :integer
end
end
class ScheduleStagesIndexMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateStageIndex'.freeze
BATCH_SIZE = 10000
disable_ddl_transaction!
class Stage < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_stages'
end
def up
disable_statement_timeout
Stage.all.tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
......@@ -322,6 +322,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
......@@ -486,6 +487,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
t.string "name"
t.integer "status"
t.integer "lock_version"
t.integer "position"
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
......
This diff is collapsed.
......@@ -10,7 +10,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
......@@ -94,7 +94,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
......
......@@ -39,7 +39,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 377,
"noteable_type": "Issue",
"noteable_iid": 377
"noteable_iid": 377,
"resolvable": false
},
{
"id": 305,
......@@ -58,7 +59,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 121,
"noteable_type": "Issue",
"noteable_iid": 121
"noteable_iid": 121,
"resolvable": false
}
]
```
......@@ -314,7 +316,8 @@ Parameters:
"system": false,
"noteable_id": 2,
"noteable_type": "MergeRequest",
"noteable_iid": 2
"noteable_iid": 2,
"resolvable": false
}
```
......
......@@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon :
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
# SVG Illustrations
......
......@@ -62,6 +62,7 @@ describe('.methodName', () => {
});
});
```
#### Testing promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
......@@ -69,9 +70,9 @@ Your Promise chain should therefore end with a call of the `done` callback and `
```javascript
// Good
it('tests a promise', (done) => {
it('tests a promise', done => {
promise
.then((data) => {
.then(data => {
expect(data).toBe(asExpected);
})
.then(done)
......@@ -79,10 +80,10 @@ it('tests a promise', (done) => {
});
// Good
it('tests a promise rejection', (done) => {
it('tests a promise rejection', done => {
promise
.then(done.fail)
.catch((error) => {
.catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
......@@ -91,38 +92,37 @@ it('tests a promise rejection', (done) => {
// Bad (missing done callback)
it('tests a promise', () => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
promise.then(data => {
expect(data).toBe(asExpected);
});
});
// Bad (missing catch)
it('tests a promise', (done) => {
it('tests a promise', done => {
promise
.then((data) => {
.then(data => {
expect(data).toBe(asExpected);
})
.then(done)
.then(done);
});
// Bad (use done.fail in asynchronous tests)
it('tests a promise', (done) => {
it('tests a promise', done => {
promise
.then((data) => {
.then(data => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(fail)
.catch(fail);
});
// Bad (missing catch)
it('tests a promise rejection', (done) => {
it('tests a promise rejection', done => {
promise
.catch((error) => {
.catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
.then(done);
});
```
......@@ -139,7 +139,7 @@ documentation for these methods can be found in the [jasmine introduction page](
Sometimes you may need to spy on a method that is directly imported by another
module. GitLab has a custom `spyOnDependency` method which utilizes
[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
achieve this. It can be used like so:
achieve this. It can be used like so:
```javascript
// my_module.js
......@@ -181,8 +181,8 @@ See this [section][vue-test].
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
* `rake karma:fixtures` (re-)generates fixtures
* `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
......@@ -217,6 +217,14 @@ yarn karma-start --filter-spec profile/account/components/
yarn karma-start -f vue_shared -f vue_mr_widget
```
You can also use glob syntax to match files. Remember to put quotes around the
glob otherwise your shell may split it into multiple arguments:
```bash
# Run all specs named `file_spec` within the IDE subdirectory
yarn karma -f 'spec/javascripts/ide/**/file_spec.js'
```
## RSpec feature integration tests
Information on setting up and running RSpec integration tests with
......@@ -231,14 +239,14 @@ supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
- Array.first
- Async functions
- Generators
- Array destructuring
- For..Of
- Symbol/Symbol.iterator
- Spread
* Array.from
* Array.first
* Async functions
* Generators
* Array destructuring
* For..Of
* Symbol/Symbol.iterator
* Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
......@@ -295,11 +303,11 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.github.io/
[vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
[rspec]: https://github.com/rspec/rspec-rails#feature-specs
[capybara]: https://github.com/teamcapybara/capybara
[karma]: http://karma-runner.github.io/
[jasmine]: https://jasmine.github.io/
---
......
# Active Sessions
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867)
> in GitLab 10.8.
GitLab lists all devices that have logged into your account. This allows you to
review the sessions and revoke any of it that you don't recognize.
## Listing all active sessions
1. On the upper right corner, click on your avatar and go to your **Settings**.
1. Navigate to the **Active Sessions** tab.
![Active sessions list](img/active_sessions_list.png)
## Revoking a session
1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
1. Click on **Revoke** besides a session. The current session cannot be
revoked, as this would sign you out of GitLab.
......@@ -39,6 +39,7 @@ From there, you can:
- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience
- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account
## Changing your username
......
......@@ -251,13 +251,4 @@ It is possible to host LFS objects externally by setting a custom LFS url with `
Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project).
```bash
curl --request PUT \
--url https://example.com/api/v4/projects/<PROJECT_ID> \
--header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
--data 'lfs_enabled=false'
```
Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
LFS can be disabled from the [Project settings](../../user/project/settings/index.md).
Feature: Project Source Markdown Render
Background:
Given I sign in as a user
And I own project "Delta"
And I visit markdown branch
# Tree README
@javascript
Scenario: Tree view should have correct links in README
Given I go directory which contains README file
And I click on a relative link in README
Then I should see the correct markdown
@javascript
Scenario: I browse files from markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Gitlab API in README
Then I should see correct document rendered
@javascript
Scenario: I view README in markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered
@javascript
Scenario: I view README in markdown branch to see reference links to directory
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on GitLab API doc directory in README
Then I should see correct doc/api directory rendered
@javascript
Scenario: I view README in markdown branch to see reference links to file
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Maintenance in README
Then I should see correct maintenance file rendered
@javascript
Scenario: README headers should have header links
Then I should see rendered README which contains correct links
And Header "Application details" should have correct id and link
# Blob
@javascript
Scenario: I navigate to doc directory to view documentation in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on users in doc/api/README
Then I should see the correct document file
@javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on raketasks in doc/api/README
Then I should see correct directory rendered
@javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And Header "GitLab API" should have correct id and link
# Markdown branch
@javascript
Scenario: I browse files from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
And I should see rendered README which contains correct links
And I click on Gitlab API in README
Then I should see correct document rendered for markdown branch
@javascript
Scenario: I browse directory from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered for markdown branch
@javascript
Scenario: I navigate to doc directory to view documentation in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
And I see correct file rendered in markdown branch
And I click on users in doc/api/README
Then I should see the users document file in markdown branch
@javascript
Scenario: I navigate to doc directory to view user doc in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
And I see correct file rendered in markdown branch
And I click on raketasks in doc/api/README
Then I should see correct directory rendered for markdown branch
@javascript
Scenario: Tree markdown links view empty urls should have correct urls
When I visit markdown branch
Then The link with text "empty" should have url "tree/markdown"
When I visit markdown branch "README.md" blob
Then The link with text "empty" should have url "blob/markdown/README.md"
When I visit markdown branch "d" tree
Then The link with text "empty" should have url "tree/markdown/d"
When I visit markdown branch "d/README.md" blob
Then The link with text "empty" should have url "blob/markdown/d/README.md"
# "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
# which Spinach interprets as the start of a comment.
@javascript
Scenario: All markdown links with ids should have correct urls
When I visit markdown branch
Then The link with text "ID" should have url "tree/markdownID"
Then The link with text "/ID" should have url "tree/markdownID"
Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
When I visit markdown branch "README.md" blob
Then The link with text "ID" should have url "blob/markdown/README.mdID"
Then The link with text "/ID" should have url "blob/markdown/README.mdID"
Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
# Wiki
Scenario: I create a wiki page with different links
Given I go to wiki page
And I add various links to the wiki page
Then Wiki page should have added links
And I click on test link
Then I see new wiki page named test
When I go back to wiki page home
And I click on GitLab API doc link
Then I see Gitlab API document
When I go back to wiki page home
And I click on Rake tasks link
Then I see Rake tasks directory
Scenario: Wiki headers should have should have ids generated for them.
Given I go to wiki page
And I add a header to the wiki page
Then Wiki header should have correct id and link
This diff is collapsed.
module SharedMarkdown
include Spinach::DSL
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
node = find("#{parent} h#{level} a#user-content-#{id}")
expect(node[:href]).to end_with "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element
expect(find(:xpath, "#{node.path}/..").text).to eq text
end
step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible
end
......
......@@ -5,11 +5,12 @@ module API
before { authenticate! }
NOTEABLE_TYPES = [Issue, Snippet].freeze
NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze
NOTEABLE_TYPES.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
......@@ -19,14 +20,12 @@ module API
success Entities::Discussion
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/discussions" do
get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
notes = noteable.notes
.inc_relations_for_view
.includes(:noteable)
......@@ -43,13 +42,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id" do
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
if notes.empty?
break not_found!("Discussion")
end
......@@ -62,19 +61,36 @@ module API
success Entities::Discussion
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
optional :position, type: Hash do
requires :base_sha, type: String, desc: 'Base commit SHA in the source branch'
requires :start_sha, type: String, desc: 'SHA referencing commit in target branch'
requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request'
requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image)
optional :new_path, type: String, desc: 'File path after change'
optional :new_line, type: Integer, desc: 'Line number after change'
optional :old_path, type: String, desc: 'File path before change'
optional :old_line, type: Integer, desc: 'Line number before change'
optional :width, type: Integer, desc: 'Width of the image'
optional :height, type: Integer, desc: 'Height of the image'
optional :x, type: Integer, desc: 'X coordinate in the image'
optional :y, type: Integer, desc: 'Y coordinate in the image'
end
end
post ":id/#{noteables_str}/:noteable_id/discussions" do
post ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
type = params[:position] ? 'DiffNote' : 'DiscussionNote'
id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id
opts = {
note: params[:body],
created_at: params[:created_at],
type: 'DiscussionNote',
type: type,
noteable_type: noteables_str.classify,
noteable_id: noteable.id
position: params[:position],
id_key => noteable.id
}
note = create_note(noteable, opts)
......@@ -91,13 +107,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
if notes.empty?
break not_found!("Notes")
end
......@@ -108,12 +124,12 @@ module API
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
......@@ -139,11 +155,11 @@ module API
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
get_note(noteable, params[:note_id])
......@@ -153,30 +169,52 @@ module API
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
requires :body, type: String, desc: 'The content of a note'
optional :body, type: String, desc: 'The content of a note'
optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved'
exactly_one_of :body, :resolved
end
put ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
update_note(noteable, params[:note_id])
if params[:resolved].nil?
update_note(noteable, params[:note_id])
else
resolve_note(noteable, params[:note_id], params[:resolved])
end
end
desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s)
desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
params do
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
end
put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
resolve_discussion(noteable, params[:discussion_id], params[:resolved])
end
end
end
end
......
......@@ -286,6 +286,10 @@ module API
end
end
class DiffRefs < Grape::Entity
expose :base_sha, :head_sha, :start_sha
end
class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
......@@ -601,6 +605,8 @@ module API
merge_request.metrics&.pipeline
end
expose :diff_refs, using: Entities::DiffRefs
def build_available?(options)
options[:project]&.feature_available?(:builds, options[:current_user])
end
......@@ -642,6 +648,11 @@ module API
expose :id, :key, :created_at
end
class DiffPosition < Grape::Entity
expose :base_sha, :start_sha, :head_sha, :old_path, :new_path,
:position_type
end
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
......@@ -655,6 +666,14 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
expose :position, if: ->(note, options) { note.diff_note? } do |note|
note.position.to_h
end
expose :resolvable?, as: :resolvable
expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? }
expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? }
# Avoid N+1 queries as much as possible
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
......
......@@ -37,13 +37,11 @@ module API
use :pagination
end
def find_groups(params)
find_params = {
all_available: params[:all_available],
custom_attributes: params[:custom_attributes],
owned: params[:owned]
}
find_params[:parent] = find_group!(params[:id]) if params[:id]
def find_groups(params, parent_id = nil)
find_params = params.slice(:all_available, :custom_attributes, :owned)
find_params[:parent] = find_group!(parent_id) if parent_id
find_params[:all_available] =
find_params.fetch(:all_available, current_user&.full_private_access?)
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
......@@ -85,7 +83,7 @@ module API
use :with_custom_attributes
end
get do
groups = find_groups(params)
groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
......@@ -213,7 +211,7 @@ module API
use :with_custom_attributes
end
get ":id/subgroups" do
groups = find_groups(params)
groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
......
......@@ -171,6 +171,10 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
def find_project_commit(id)
user_project.commit_by(oid: id)
end
def find_project_snippet(id)
finder_params = { project: user_project }
SnippetsFinder.new(current_user, finder_params).find(id)
......
......@@ -7,6 +7,9 @@ module API
helpers do
params :with_custom_attributes do
optional :with_custom_attributes, type: Boolean, default: false, desc: 'Include custom attributes in the response'
optional :custom_attributes, type: Hash,
desc: 'Filter with custom attributes'
end
def with_custom_attributes(collection_or_resource, options = {})
......
......@@ -21,6 +21,23 @@ module API
end
end
def resolve_note(noteable, note_id, resolved)
note = noteable.notes.find(note_id)
authorize! :resolve_note, note
bad_request!("Note is not resolvable") unless note.resolvable?
if resolved
parent = noteable_parent(noteable)
::Notes::ResolveService.new(parent, current_user).execute(note)
else
note.unresolve!
end
present note, with: Entities::Note
end
def delete_note(noteable, note_id)
note = noteable.notes.find(note_id)
......@@ -35,7 +52,7 @@ module API
def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
can_read_note = !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
......@@ -49,7 +66,20 @@ module API
end
def find_noteable(parent, noteables_str, noteable_id)
public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
readable =
if noteable.is_a?(Commit)
# for commits there is not :read_commit policy, check if user
# has :read_note permission on the commit's project
can?(current_user, :read_note, user_project)
else
can?(current_user, noteable_read_ability_name(noteable), noteable)
end
return not_found!(noteables_str) unless readable
noteable
end
def noteable_parent(noteable)
......@@ -57,11 +87,8 @@ module API
end
def create_note(noteable, opts)
noteables_str = noteable.model_name.to_s.underscore.pluralize
return not_found!(noteables_str) unless can?(current_user, noteable_read_ability_name(noteable), noteable)
authorize! :create_note, noteable
policy_object = noteable.is_a?(Commit) ? user_project : noteable
authorize!(:create_note, policy_object)
parent = noteable_parent(noteable)
......@@ -73,6 +100,21 @@ module API
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, current_user, opts).execute
end
def resolve_discussion(noteable, discussion_id, resolved)
discussion = noteable.find_discussion(discussion_id)
forbidden! unless discussion.can_resolve?(current_user)
if resolved
parent = noteable_parent(noteable)
::Discussions::ResolveService.new(parent, current_user, merge_request: noteable).execute(discussion)
else
discussion.unresolve!
end
present discussion, with: Entities::Discussion
end
end
end
end
......@@ -31,23 +31,19 @@ module API
get ":id/#{noteables_str}/:noteable_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
# page can have less elements than :per_page even if
# there's more than one page.
raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
notes =
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
paginate(raw_notes)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
not_found!("Notes")
end
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
# page can have less elements than :per_page even if
# there's more than one page.
raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
notes =
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
paginate(raw_notes)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
end
desc "Get a single #{noteable_type.to_s.downcase} note" do
......
......@@ -13,7 +13,6 @@ module Banzai
# * https://git.wiki.kernel.org/index.php/CommitMessageConventions
class CommitTrailersFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
include ApplicationHelper
include AvatarsHelper
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateStageIndex
def perform(start_id, stop_id)
migrate_stage_index_sql(start_id.to_i, stop_id.to_i).tap do |sql|
ActiveRecord::Base.connection.execute(sql)
end
end
private
def migrate_stage_index_sql(start_id, stop_id)
if Gitlab::Database.postgresql?
<<~SQL
WITH freqs AS (
SELECT stage_id, stage_idx, COUNT(*) AS freq FROM ci_builds
WHERE stage_id BETWEEN #{start_id} AND #{stop_id}
AND stage_idx IS NOT NULL
GROUP BY stage_id, stage_idx
), indexes AS (
SELECT DISTINCT stage_id, last_value(stage_idx)
OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
FROM freqs
)
UPDATE ci_stages SET position = indexes.index
FROM indexes WHERE indexes.stage_id = ci_stages.id
AND ci_stages.position IS NULL;
SQL
else
<<~SQL
UPDATE ci_stages
SET position =
(SELECT stage_idx FROM ci_builds
WHERE ci_builds.stage_id = ci_stages.id
GROUP BY ci_builds.stage_idx ORDER BY COUNT(*) DESC LIMIT 1)
WHERE ci_stages.id BETWEEN #{start_id} AND #{stop_id}
AND ci_stages.position IS NULL
SQL
end
end
end
end
end
......@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
@cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
@cron_timezone = timezone_name(cron_timezone)
end
def next_time_from(time)
......@@ -24,6 +24,12 @@ module Gitlab
private
def timezone_name(timezone)
ActiveSupport::TimeZone.find_tzinfo(timezone).name
rescue TZInfo::InvalidTimezoneIdentifier
timezone
end
# NOTE:
# cron_timezone can only accept timezones listed in TZInfo::Timezone.
# Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
......
......@@ -19,6 +19,7 @@ module Gitlab
def attributes
{ name: @attributes.fetch(:name),
position: @attributes.fetch(:index),
pipeline: @pipeline,
project: @pipeline.project }
end
......
module Gitlab
module Database
module ArelMethods
private
# In Arel 7.0.0 (Arel 7.1.4 is used in Rails 5.0) the `engine` parameter of `Arel::UpdateManager#initializer`
# was removed.
# Remove this file and inline this method when removing rails5? code.
def arel_update_manager
if Gitlab.rails5?
Arel::UpdateManager.new
else
Arel::UpdateManager.new(ActiveRecord::Base)
end
end
end
end
end
module Gitlab
module Database
module MigrationHelpers
include Gitlab::Database::ArelMethods
BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
......@@ -314,7 +316,7 @@ module Gitlab
stop_arel = yield table, stop_arel if block_given?
stop_row = exec_query(stop_arel.to_sql).to_hash.first
update_arel = Arel::UpdateManager.new(ActiveRecord::Base)
update_arel = arel_update_manager
.table(table)
.set([[table[column], value]])
.where(table[:id].gteq(start_id))
......
......@@ -3,6 +3,8 @@ module Gitlab
module RenameReservedPathsMigration
module V1
class RenameBase
include Gitlab::Database::ArelMethods
attr_reader :paths, :migration
delegate :update_column_in_batches,
......@@ -62,10 +64,10 @@ module Gitlab
old_full_path,
new_full_path)
update = Arel::UpdateManager.new(ActiveRecord::Base)
.table(routes)
.set([[routes[:path], replace_statement]])
.where(Arel::Nodes::SqlLiteral.new(filter))
update = arel_update_manager
.table(routes)
.set([[routes[:path], replace_statement]])
.where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql)
end
......
......@@ -36,6 +36,8 @@ module Gitlab
private
def decorate_diff!(diff)
return diff if diff.is_a?(File)
Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs, fallback_diff_refs: fallback_diff_refs)
end
end
......
......@@ -12,6 +12,10 @@ module Gitlab
:head_sha,
:old_line,
:new_line,
:width,
:height,
:x,
:y,
:position_type, to: :formatter
# A position can belong to a text line or to an image coordinate
......
......@@ -38,7 +38,9 @@ module Gitlab
end
def extract_operation
case @raw_operation&.first(1)
return :unknown unless @raw_operation
case @raw_operation[0]
when 'A'
:added
when 'C'
......
......@@ -15,6 +15,9 @@ module Gitlab
def generate_script
<<~HEREDOC
set -eo pipefail
ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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