Commit 1eb50566 authored by Felipe Artur's avatar Felipe Artur

Merge CE/master into EE/master

parents 7e5cf83f d3aaa1a2
...@@ -432,9 +432,9 @@ pages: ...@@ -432,9 +432,9 @@ pages:
script: script:
- mv public/ .public/ - mv public/ .public/
- mkdir public/ - mkdir public/
- mv coverage public/coverage-ruby - mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/default/ public/coverage-javascript/ - mv coverage-javascript/default/ public/coverage-javascript/ || true
- mv eslint-report.html public/ - mv eslint-report.html public/ || true
artifacts: artifacts:
paths: paths:
- public - public
......
...@@ -252,5 +252,7 @@ window.ES6Promise.polyfill(); ...@@ -252,5 +252,7 @@ window.ES6Promise.polyfill();
new Aside(); new Aside();
// bind sidebar events // bind sidebar events
new gl.Sidebar(); new gl.Sidebar();
gl.utils.initTimeagoTimeout();
}); });
}).call(this); }).call(this);
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
/* global UsersSelect */ /* global UsersSelect */
/* global GroupAvatar */ /* global GroupAvatar */
/* global LineHighlighter */ /* global LineHighlighter */
/* global ShortcutsBlob */
/* global ProjectFork */ /* global ProjectFork */
/* global BuildArtifacts */ /* global BuildArtifacts */
/* global GroupsSelect */ /* global GroupsSelect */
...@@ -38,6 +37,8 @@ ...@@ -38,6 +37,8 @@
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
const ShortcutsBlob = require('./shortcuts_blob');
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -223,7 +224,12 @@ ...@@ -223,7 +224,12 @@
case 'projects:blame:show': case 'projects:blame:show':
new LineHighlighter(); new LineHighlighter();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true); const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
break; break;
case 'groups:labels:new': case 'groups:labels:new':
case 'groups:labels:edit': case 'groups:labels:edit':
......
...@@ -147,12 +147,12 @@ require('./environment_terminal_button'); ...@@ -147,12 +147,12 @@ require('./environment_terminal_button');
}, },
/** /**
* Returns the value of the `stoppable?` key provided in the response. * Returns the value of the `stop_action?` key provided in the response.
* *
* @returns {Boolean} * @returns {Boolean}
*/ */
isStoppable() { hasStopAction() {
return this.model['stoppable?']; return this.model['stop_action?'];
}, },
/** /**
...@@ -508,7 +508,7 @@ require('./environment_terminal_button'); ...@@ -508,7 +508,7 @@ require('./environment_terminal_button');
</external-url-component> </external-url-component>
</div> </div>
<div v-if="isStoppable && canCreateDeployment" <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container"> class="inline js-stop-component-container">
<stop-component <stop-component
:stop-url="model.stop_path"> :stop-url="model.stop_path">
......
...@@ -69,6 +69,9 @@ ...@@ -69,6 +69,9 @@
var hash = w.gl.utils.getLocationHash(); var hash = w.gl.utils.getLocationHash();
if (!hash) return; if (!hash) return;
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
var navbar = document.querySelector('.navbar-gitlab'); var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav'); var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix'); var fixedTabs = document.querySelector('.js-tabs-affix');
...@@ -134,6 +137,14 @@ ...@@ -134,6 +137,14 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
}; };
gl.utils.isMetaClick = function(e) {
// Identify following special clicks
// 1) Cmd + Click on Mac (e.metaKey)
// 2) Ctrl + Click on PC (e.ctrlKey)
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
return e.metaKey || e.ctrlKey || e.which === 2;
};
gl.utils.scrollToElement = function($el) { gl.utils.scrollToElement = function($el) {
var top = $el.offset().top; var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
......
...@@ -8,6 +8,8 @@ window.dateFormat = require('vendor/date.format'); ...@@ -8,6 +8,8 @@ window.dateFormat = require('vendor/date.format');
(function() { (function() {
(function(w) { (function(w) {
var base; var base;
var timeagoInstance;
if (w.gl == null) { if (w.gl == null) {
w.gl = {}; w.gl = {};
} }
...@@ -24,29 +26,28 @@ window.dateFormat = require('vendor/date.format'); ...@@ -24,29 +26,28 @@ window.dateFormat = require('vendor/date.format');
return this.days[date.getDay()]; return this.days[date.getDay()];
}; };
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
if (setTimeago == null) { $timeagoEls.each((i, el) => {
setTimeago = true; el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
}
$timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
if (setTimeago) { if (setTimeago) {
// Recreate with custom template // Recreate with custom template
$el.tooltip({ $(el).tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
}); });
} }
$el.attr('data-timeago-rendered', true); el.classList.add('js-timeago-render');
gl.utils.renderTimeago($el);
}); });
gl.utils.renderTimeago($timeagoEls);
}; };
w.gl.utils.getTimeago = function() { w.gl.utils.getTimeago = function() {
var locale = function(number, index) { var locale;
if (!timeagoInstance) {
locale = function(number, index) {
return [ return [
['less than a minute ago', 'a while'], ['less than a minute ago', 'a while'],
['less than a minute ago', 'in %s seconds'], ['less than a minute ago', 'in %s seconds'],
...@@ -66,7 +67,10 @@ window.dateFormat = require('vendor/date.format'); ...@@ -66,7 +67,10 @@ window.dateFormat = require('vendor/date.format');
}; };
timeago.register('gl_en', locale); timeago.register('gl_en', locale);
return timeago(); timeagoInstance = timeago();
}
return timeagoInstance;
}; };
w.gl.utils.timeFor = function(time, suffix, expiredLabel) { w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
...@@ -85,9 +89,30 @@ window.dateFormat = require('vendor/date.format'); ...@@ -85,9 +89,30 @@ window.dateFormat = require('vendor/date.format');
return timefor; return timefor;
}; };
w.gl.utils.renderTimeago = function($element) { w.gl.utils.cachedTimeagoElements = [];
var timeagoInstance = gl.utils.getTimeago(); w.gl.utils.renderTimeago = function($els) {
timeagoInstance.render($element, 'gl_en'); if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const timeago = gl.utils.getTimeago();
const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
}; };
w.gl.utils.getDayDifference = function(a, b) { w.gl.utils.getDayDifference = function(a, b) {
......
...@@ -82,12 +82,18 @@ require('./flash'); ...@@ -82,12 +82,18 @@ require('./flash');
$(document) $(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab); .on('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.on('click', this.clickTab);
} }
unbindEvents() { unbindEvents() {
$(document) $(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab); .off('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.off('click', this.clickTab);
} }
showTab(e) { showTab(e) {
...@@ -95,6 +101,14 @@ require('./flash'); ...@@ -95,6 +101,14 @@ require('./flash');
this.activateTab($(e.target).data('action')); this.activateTab($(e.target).data('action'));
} }
clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href');
e.stopImmediatePropagation();
window.open(targetLink, '_blank');
}
}
tabShown(e) { tabShown(e) {
const $target = $(e.target); const $target = $(e.target);
const action = $target.data('action'); const action = $target.data('action');
......
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
/* global Shortcuts */
/* global Mousetrap */
require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.ShortcutsBlob = (function(superClass) {
extend(ShortcutsBlob, superClass);
function ShortcutsBlob(skipResetBindings) {
ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
}
ShortcutsBlob.copyToClipboard = function() {
var clipboardButton;
clipboardButton = $('.btn-clipboard');
if (clipboardButton) {
return clipboardButton.click();
}
};
return ShortcutsBlob;
})(Shortcuts);
}).call(this);
/* global Mousetrap */
/* global Shortcuts */
require('./shortcuts');
const defaults = {
skipResetBindings: false,
fileBlobPermalinkUrl: null,
};
class ShortcutsBlob extends Shortcuts {
constructor(opts) {
const options = Object.assign({}, defaults, opts);
super(options.skipResetBindings);
this.options = options;
Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
}
moveToFilePermalink() {
if (this.options.fileBlobPermalinkUrl) {
const hash = gl.utils.getLocationHash();
const hashUrlString = hash ? `#${hash}` : '';
gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
}
}
}
module.exports = ShortcutsBlob;
...@@ -146,14 +146,26 @@ ...@@ -146,14 +146,26 @@
} }
goToTodoUrl(e) { goToTodoUrl(e) {
const todoLink = $(this).data('url'); const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) { if (!todoLink) {
return; return;
} }
// Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) { if (gl.utils.isMetaClick(e)) {
e.preventDefault(); e.preventDefault();
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually.
if (targetLink !== todoLink) {
return window.open(targetLink, '_blank');
} else {
return window.open(todoLink, '_blank'); return window.open(todoLink, '_blank');
}
} else { } else {
return gl.utils.visitUrl(todoLink); return gl.utils.visitUrl(todoLink);
} }
......
...@@ -313,3 +313,7 @@ ul.controls { ...@@ -313,3 +313,7 @@ ul.controls {
} }
} }
} }
ul.indent-list {
padding: 10px 0 0 30px;
}
...@@ -193,7 +193,7 @@ ...@@ -193,7 +193,7 @@
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 10; z-index: 8;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
......
...@@ -148,3 +148,7 @@ ul.related-merge-requests > li { ...@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
} }
} }
.recaptcha {
margin-bottom: 30px;
}
...@@ -259,3 +259,8 @@ ...@@ -259,3 +259,8 @@
} }
} }
} }
.label-link {
display: inline-block;
vertical-align: text-top;
}
...@@ -229,6 +229,7 @@ ...@@ -229,6 +229,7 @@
.finished-at { .finished-at {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
margin: 4px 0; margin: 4px 0;
white-space: nowrap;
.fa { .fa {
font-size: 12px; font-size: 12px;
...@@ -666,7 +667,7 @@ ...@@ -666,7 +667,7 @@
vertical-align: bottom; vertical-align: bottom;
display: inline-block; display: inline-block;
position: relative; position: relative;
font-weight: 200; font-weight: normal;
} }
// Dropdown button in mini pipeline graph // Dropdown button in mini pipeline graph
......
module SpammableActions module SpammableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Recaptcha::Verify
included do included do
before_action :authorize_submit_spammable!, only: :mark_as_spam before_action :authorize_submit_spammable!, only: :mark_as_spam
end end
...@@ -15,6 +17,15 @@ module SpammableActions ...@@ -15,6 +17,15 @@ module SpammableActions
private private
def recaptcha_params
return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
{
recaptcha_verified: true,
spam_log_id: params[:spam_log_id]
}
end
def spammable def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}" raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end end
...@@ -22,4 +33,11 @@ module SpammableActions ...@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable! def authorize_submit_spammable!
access_denied! unless current_user.admin? access_denied! unless current_user.admin?
end end
def render_recaptcha?
return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
return false unless Gitlab::Recaptcha.enabled?
spammable.spam
end
end end
...@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
def stop def stop
return render_404 unless @environment.stoppable? return render_404 unless @environment.available?
new_action = @environment.stop!(current_user) stop_action = @environment.stop_with_action!(current_user)
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
if stop_action
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
else
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
end
end end
def terminal def terminal
......
...@@ -98,15 +98,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -98,15 +98,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create def create
extra_params = { request: request, extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
extra_params.merge!(recaptcha_params)
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
if @issue.valid? html_response_create
redirect_to issue_path(@issue)
else
render :new
end
end end
format.js do format.js do
@link = @issue.attachment.url.to_js @link = @issue.attachment.url.to_js
...@@ -183,6 +181,20 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -183,6 +181,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def html_response_create
if @issue.valid?
redirect_to issue_path(@issue)
elsif render_recaptcha?
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
render :verify
else
render :new
end
end
def issue def issue
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
......
...@@ -480,7 +480,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -480,7 +480,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url = stop_url =
if environment.stoppable? && can?(current_user, :create_deployment, environment) if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment) stop_namespace_project_environment_path(project.namespace, project, environment)
end end
......
...@@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super super
else else
flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error flash.delete :recaptcha_error
render action: 'new' render action: 'new'
end end
......
...@@ -20,8 +20,8 @@ module MergeRequestsHelper ...@@ -20,8 +20,8 @@ module MergeRequestsHelper
end end
def mr_widget_refresh_url(mr) def mr_widget_refresh_url(mr)
if mr && mr.source_project if mr && mr.target_project
merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else else
'' ''
end end
......
...@@ -11,6 +11,7 @@ module Spammable ...@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam attr_accessor :spam
attr_accessor :spam_log
after_validation :check_for_spam, on: :create after_validation :check_for_spam, on: :create
...@@ -34,9 +35,14 @@ module Spammable ...@@ -34,9 +35,14 @@ module Spammable
end end
def check_for_spam def check_for_spam
if spam? error_msg = if Gitlab::Recaptcha.enabled?
self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") "Your #{spammable_entity_type} has been recognized as spam. "\
"You can still submit it by solving Captcha."
else
"Your #{spammable_entity_type} has been recognized as spam and has been discarded."
end end
self.errors.add(:base, error_msg) if spam?
end end
def spammable_entity_type def spammable_entity_type
......
...@@ -18,7 +18,7 @@ module TimeTrackable ...@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent validate :check_negative_time_spent
has_many :timelogs, as: :trackable, dependent: :destroy has_many :timelogs, dependent: :destroy
end end
def spend_time(options) def spend_time(options)
......
...@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base ...@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop) @stop_action ||= manual_actions.find_by(name: on_stop)
end end
def stoppable? def stop_action?
stop_action.present? stop_action.present?
end end
......
...@@ -110,15 +110,15 @@ class Environment < ActiveRecord::Base ...@@ -110,15 +110,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '') external_url.gsub(/\A.*?:\/\//, '')
end end
def stoppable? def stop_action?
available? && stop_action.present? available? && stop_action.present?
end end
def stop!(current_user) def stop_with_action!(current_user)
return unless stoppable? return unless available?
stop stop!
stop_action.play(current_user) stop_action.play(current_user) if stop_action
end end
def actions_for(environment) def actions_for(environment)
......
...@@ -241,7 +241,12 @@ class Group < Namespace ...@@ -241,7 +241,12 @@ class Group < Namespace
end end
def refresh_members_authorized_projects def refresh_members_authorized_projects
UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
execute
end
def user_ids_for_project_authorizations
users_with_parents.pluck(:id)
end end
def members_with_parents def members_with_parents
......
...@@ -218,6 +218,10 @@ class Namespace < ActiveRecord::Base ...@@ -218,6 +218,10 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end end
def user_ids_for_project_authorizations
[owner_id]
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service ...@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: '' } { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
] ]
end end
......
...@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService ...@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end end
def title def title
'Mattermost Command' 'Mattermost slash commands'
end end
def description def description
"Perform common operations on GitLab in Mattermost" "Perform common operations in Mattermost"
end end
def self.to_param def self.to_param
......
...@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService ...@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper include TriggersHelper
def title def title
'Slack Command' 'Slack slash commands'
end end
def description def description
"Perform common operations on GitLab in Slack" "Perform common operations in Slack"
end end
def self.to_param def self.to_param
......
class Timelog < ActiveRecord::Base class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true validates :time_spent, :user, presence: true
validate :issuable_id_is_present
belongs_to :trackable, polymorphic: true belongs_to :issue
belongs_to :merge_request
belongs_to :user belongs_to :user
def issuable
issue || merge_request
end
private
def issuable_id_is_present
if issue_id && merge_request_id
errors.add(:base, 'Only Issue ID or Merge Request ID is required')
elsif issuable.nil?
errors.add(:base, 'Issue or Merge Request ID is required')
end
end
end end
...@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url expose :external_url
expose :environment_type expose :environment_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stoppable? expose :stop_action?
expose :environment_path do |environment| expose :environment_path do |environment|
namespace_project_environment_path( namespace_project_environment_path(
......
...@@ -8,10 +8,9 @@ module Ci ...@@ -8,10 +8,9 @@ module Ci
return unless has_ref? return unless has_ref?
environments.each do |environment| environments.each do |environment|
next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project) next unless can?(current_user, :create_deployment, project)
environment.stop!(current_user) environment.stop_with_action!(current_user)
end end
end end
......
...@@ -3,6 +3,8 @@ module Issues ...@@ -3,6 +3,8 @@ module Issues
def execute def execute
@request = params.delete(:request) @request = params.delete(:request)
@api = params.delete(:api) @api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute @issue = BuildService.new(project, current_user, issue_attributes).execute
...@@ -11,7 +13,13 @@ module Issues ...@@ -11,7 +13,13 @@ module Issues
end end
def before_create(issuable) def before_create(issuable)
if @recaptcha_verified
spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
spam_log.update!(recaptcha_verified: true) if spam_log
else
issuable.spam = spam_service.check(@api) issuable.spam = spam_service.check(@api)
issuable.spam_log = spam_service.spam_log
end
end end
def after_create(issuable) def after_create(issuable)
...@@ -35,7 +43,7 @@ module Issues ...@@ -35,7 +43,7 @@ module Issues
private private
def spam_service def spam_service
SpamService.new(@issue, @request) @spam_service ||= SpamService.new(@issue, @request)
end end
def user_agent_detail_service def user_agent_detail_service
......
...@@ -25,9 +25,10 @@ module Projects ...@@ -25,9 +25,10 @@ module Projects
end end
def transfer(project, new_namespace) def transfer(project, new_namespace)
old_namespace = project.namespace
Project.transaction do Project.transaction do
old_path = project.path_with_namespace old_path = project.path_with_namespace
old_namespace = project.namespace
old_group = project.group old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path) new_path = File.join(new_namespace.try(:path) || '', project.path)
...@@ -70,8 +71,11 @@ module Projects ...@@ -70,8 +71,11 @@ module Projects
project.old_path_with_namespace = old_path project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end end
refresh_permissions(old_namespace, new_namespace)
true
end end
def allowed_transfer?(current_user, project, namespace) def allowed_transfer?(current_user, project, namespace)
...@@ -80,5 +84,14 @@ module Projects ...@@ -80,5 +84,14 @@ module Projects
namespace.id != project.namespace_id && namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, namespace)
end end
def refresh_permissions(old_namespace, new_namespace)
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute
end
end end
end end
class SpamService class SpamService
attr_accessor :spammable, :request, :options attr_accessor :spammable, :request, :options
attr_reader :spam_log
def initialize(spammable, request = nil) def initialize(spammable, request = nil)
@spammable = spammable @spammable = spammable
...@@ -63,7 +64,7 @@ class SpamService ...@@ -63,7 +64,7 @@ class SpamService
end end
def create_spam_log(api) def create_spam_log(api)
SpamLog.create( @spam_log = SpamLog.create!(
{ {
user_id: spammable_owner_id, user_id: spammable_owner_id,
title: spammable.spam_title, title: spammable.spam_title,
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
= spam_log.source_ip = spam_log.source_ip
%td %td
= spam_log.via_api? ? 'Y' : 'N' = spam_log.via_api? ? 'Y' : 'N'
%td
= spam_log.recaptcha_verified ? 'Y' : 'N'
%td %td
= spam_log.noteable_type = spam_log.noteable_type
%td %td
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
%th User %th User
%th Source IP %th Source IP
%th API? %th API?
%th Recaptcha verified?
%th Type %th Type
%th Title %th Title
%th Description %th Description
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div %div
- if current_application_settings.recaptcha_enabled - if Gitlab::Recaptcha.enabled?
= recaptcha_tags = recaptcha_tags
%div %div
= f.submit "Register", class: "btn-register btn" = f.submit "Register", class: "btn-register btn"
......
...@@ -79,6 +79,14 @@ ...@@ -79,6 +79,14 @@
%td.shortcut %td.shortcut
.key esc .key esc
%td Go back %td Go back
%tbody
%tr
%th
%th Project File
%tr
%td.shortcut
.key y
%td Go to file permalink
.col-lg-4 .col-lg-4
%table.shortcut-mappings %table.shortcut-mappings
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm' class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
tree_join(@commit.sha, @path)), class: 'btn btn-sm' tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user - if current_user
.btn-group{ role: "group" } .btn-group{ role: "group" }
......
- if can?(current_user, :create_deployment, environment) && environment.stoppable? - if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline .inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post, = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= render 'projects/environments/external_url', environment: @environment = render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- if can?(current_user, :create_deployment, @environment) && @environment.stoppable? - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container .deployments-container
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
- if issue.labels.any? - if issue.labels.any?
&nbsp; &nbsp;
- issue.labels.each do |label| - issue.labels.each do |label|
= link_to_label(label, subject: issue.project) = link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.tasks? - if issue.tasks?
&nbsp; &nbsp;
%span.task-status %span.task-status
......
- page_title "Anti-spam verification"
%h3.page-title
Anti-spam verification
%hr
%p
We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
.recaptcha
- params[:issue].each do |field, value|
= hidden_field(:issue, field, value: value)
= hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
= hidden_field_tag(:spam_log_id, @issue.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
.row-content-block.footer-block
= f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
- if merge_request.labels.any? - if merge_request.labels.any?
&nbsp; &nbsp;
- merge_request.labels.each do |label| - merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request) = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
- if merge_request.tasks? - if merge_request.tasks?
&nbsp; &nbsp;
......
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" - run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
To setup this service: %p To setup this service:
%ul.list-unstyled %ul.list-unstyled.indent-list
%li %li
1. 1.
= link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Enable custom slash commands
= icon('external-link')
on your Mattermost installation on your Mattermost installation
%li %li
2. 2.
= link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
in Mattermost with these options: Add a slash command
= icon('external-link')
in your Mattermost team with these options:
%hr %hr
.help-form .help-form
...@@ -83,9 +86,14 @@ To setup this service: ...@@ -83,9 +86,14 @@ To setup this service:
%hr %hr
%ul.list-unstyled %ul.list-unstyled.indent-list
%li %li
3. After adding the slash command, paste the 3. Paste the
%strong Token
%strong token
into the field below into the field below
%li
4. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Mattermost!
- enabled = Gitlab.config.mattermost.enabled - enabled = Gitlab.config.mattermost.enabled
.well .well
This service allows GitLab users to perform common operations on this %p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost. project by entering slash commands in Mattermost.
%br = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Mattermost after setting up this service, See list of available commands in Mattermost after setting up this service,
by entering by entering
%code /&lt;command_trigger_word&gt; help %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template? - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
......
- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path" - pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
- run_actions_text = "Perform common operations on this project: #{pretty_name}" - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well .well
This service allows GitLab users to perform common operations on this %p
This service allows users to perform common operations on this
project by entering slash commands in Slack. project by entering slash commands in Slack.
%br = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Slack after setting up this service, See list of available commands in Slack after setting up this service,
by entering by entering
%code /&lt;command&gt; help %kbd.inline /&lt;command&gt; help
%br
%br
- unless @service.template? - unless @service.template?
To setup this service: %p To setup this service:
%ul.list-unstyled %ul.list-unstyled.indent-list
%li %li
1. 1.
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
= icon('external-link')
in your Slack team with these options: in your Slack team with these options:
%hr %hr
...@@ -82,7 +86,7 @@ ...@@ -82,7 +86,7 @@
%hr %hr
%ul.list-unstyled %ul.list-unstyled.indent-list
%li %li
2. Paste the 2. Paste the
%strong Token %strong Token
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline .filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul %ul
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline .filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul %ul
......
---
title: Use reCaptcha when an issue is identified as a spam
merge_request: 8846
author:
---
title: Adds back ability to stop all environments
merge_request: 7379
author:
---
title: Fix Ctrl+Click support for Todos and Merge Request page tabs
merge_request: 8898
author:
---
title: Align Segoe UI label text
merge_request:
author:
---
title: Refactor Timelogs structure to use foreign keys.
merge_request: 8769
author:
---
title: 27352-search-label-filter-header
merge_request:
author:
---
title: Fix MR widget url
merge_request: 8989
author:
---
title: Layer award emoji dropdown over the right sidebar
merge_request: 9004
author:
---
title: Give ci status text on pipeline graph a better font-weight
merge_request:
author:
---
title: Add `y` keyboard shortcut to move to file permalink
merge_request:
author:
---
title: Fix broken anchor links when special characters are used
merge_request: 8961
author: Andrey Krivko
---
title: Refresh authorizations when transferring projects
merge_request:
author:
---
title: 'API: Remove /projects/:id/keys/.. endpoints'
merge_request: 8716
author: Robert Schilling
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddForeignKeysToTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
change_table :timelogs do |t|
t.column :issue_id, :integer
t.column :merge_request_id, :integer
end
add_concurrent_index :timelogs, :issue_id
add_concurrent_index :timelogs, :merge_request_id
if Gitlab::Database.postgresql?
execute <<-EOF
ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
EOF
else
execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
end
Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
end
def down
Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
remove_columns :timelogs, :issue_id, :merge_request_id
end
end
class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false)
end
def down
remove_column(:spam_logs, :recaptcha_verified)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
remove_columns :timelogs, :trackable_id, :trackable_type
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
if Gitlab::Database.postgresql?
execute <<-EOF
ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
EOF
end
end
def down
# noop
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170204181513) do ActiveRecord::Schema.define(version: 20170206101030) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1296,6 +1296,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do ...@@ -1296,6 +1296,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false t.boolean "submitted_as_ham", default: false, null: false
t.boolean "recaptcha_verified", default: false, null: false
end end
create_table "subscriptions", force: :cascade do |t| create_table "subscriptions", force: :cascade do |t|
...@@ -1332,14 +1333,15 @@ ActiveRecord::Schema.define(version: 20170204181513) do ...@@ -1332,14 +1333,15 @@ ActiveRecord::Schema.define(version: 20170204181513) do
create_table "timelogs", force: :cascade do |t| create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false t.integer "time_spent", null: false
t.integer "trackable_id"
t.string "trackable_type"
t.integer "user_id" t.integer "user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "issue_id"
t.integer "merge_request_id"
end end
add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
create_table "todos", force: :cascade do |t| create_table "todos", force: :cascade do |t|
...@@ -1534,6 +1536,8 @@ ActiveRecord::Schema.define(version: 20170204181513) do ...@@ -1534,6 +1536,8 @@ ActiveRecord::Schema.define(version: 20170204181513) do
add_foreign_key "protected_branch_push_access_levels", "users" add_foreign_key "protected_branch_push_access_levels", "users"
add_foreign_key "remote_mirrors", "projects" add_foreign_key "remote_mirrors", "projects"
add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
end end
...@@ -11,3 +11,4 @@ changes are in V4: ...@@ -11,3 +11,4 @@ changes are in V4:
- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` - `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) - Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
- Project snippets do not return deprecated field `expires_at` - Project snippets do not return deprecated field `expires_at`
- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
...@@ -5,6 +5,7 @@ module API ...@@ -5,6 +5,7 @@ module API
version %w(v3 v4), using: :path version %w(v3 v4), using: :path
version 'v3', using: :path do version 'v3', using: :path do
mount ::API::V3::DeployKeys
mount ::API::V3::Issues mount ::API::V3::Issues
mount ::API::V3::MergeRequests mount ::API::V3::MergeRequests
mount ::API::V3::Projects mount ::API::V3::Projects
......
module API module API
# Projects API
class DeployKeys < Grape::API class DeployKeys < Grape::API
before { authenticate! } before { authenticate! }
...@@ -16,14 +15,10 @@ module API ...@@ -16,14 +15,10 @@ module API
resource :projects do resource :projects do
before { authorize_admin_project } before { authorize_admin_project }
# Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "projects/:id/deploy_keys/..." instead.
#
%w(keys deploy_keys).each do |path|
desc "Get a specific project's deploy keys" do desc "Get a specific project's deploy keys" do
success Entities::SSHKey success Entities::SSHKey
end end
get ":id/#{path}" do get ":id/deploy_keys" do
present user_project.deploy_keys, with: Entities::SSHKey present user_project.deploy_keys, with: Entities::SSHKey
end end
...@@ -33,7 +28,7 @@ module API ...@@ -33,7 +28,7 @@ module API
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end
get ":id/#{path}/:key_id" do get ":id/deploy_keys/:key_id" do
key = user_project.deploy_keys.find params[:key_id] key = user_project.deploy_keys.find params[:key_id]
present key, with: Entities::SSHKey present key, with: Entities::SSHKey
end end
...@@ -45,7 +40,7 @@ module API ...@@ -45,7 +40,7 @@ module API
requires :key, type: String, desc: 'The new deploy key' requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key' requires :title, type: String, desc: 'The name of the deploy key'
end end
post ":id/#{path}" do post ":id/deploy_keys" do
params[:key].strip! params[:key].strip!
# Check for an existing key joined to this project # Check for an existing key joined to this project
...@@ -79,7 +74,7 @@ module API ...@@ -79,7 +74,7 @@ module API
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end
post ":id/#{path}/:key_id/enable" do post ":id/deploy_keys/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project, key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute current_user, declared_params).execute
...@@ -97,7 +92,7 @@ module API ...@@ -97,7 +92,7 @@ module API
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end
delete ":id/#{path}/:key_id/disable" do delete ":id/deploy_keys/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy key.destroy
...@@ -110,7 +105,7 @@ module API ...@@ -110,7 +105,7 @@ module API
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end
delete ":id/#{path}/:key_id" do delete ":id/deploy_keys/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key if key
key.destroy key.destroy
...@@ -120,5 +115,4 @@ module API ...@@ -120,5 +115,4 @@ module API
end end
end end
end end
end
end end
module API
module V3
class DeployKeys < Grape::API
before { authenticate! }
get "deploy_keys" do
authenticated_as_admin!
keys = DeployKey.all
present keys, with: ::API::Entities::SSHKey
end
params do
requires :id, type: String, desc: 'The ID of the project'
end
resource :projects do
before { authorize_admin_project }
%w(keys deploy_keys).each do |path|
desc "Get a specific project's deploy keys" do
success ::API::Entities::SSHKey
end
get ":id/#{path}" do
present user_project.deploy_keys, with: ::API::Entities::SSHKey
end
desc 'Get single deploy key' do
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
get ":id/#{path}/:key_id" do
key = user_project.deploy_keys.find params[:key_id]
present key, with: ::API::Entities::SSHKey
end
desc 'Add new deploy key to currently authenticated user' do
success ::API::Entities::SSHKey
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
end
post ":id/#{path}" do
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: ::API::Entities::SSHKey
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: ::API::Entities::SSHKey
break
end
# Create a new deploy key
key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: ::API::Entities::SSHKey
else
render_validation_error!(key)
end
end
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute
if key
present key, with: ::API::Entities::SSHKey
else
not_found!('Deploy Key')
end
end
desc 'Disable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy
present key.deploy_key, with: ::API::Entities::SSHKey
end
desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key
key.destroy
else
not_found!('Deploy Key')
end
end
end
end
end
end
end
...@@ -10,5 +10,9 @@ module Gitlab ...@@ -10,5 +10,9 @@ module Gitlab
true true
end end
end end
def self.enabled?
current_application_settings.recaptcha_enabled
end
end end
end end
...@@ -163,20 +163,20 @@ namespace :gitlab do ...@@ -163,20 +163,20 @@ namespace :gitlab do
namespace :pages do namespace :pages do
task create: :environment do task create: :environment do
$progress.puts "Dumping pages ... ".blue $progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages") if ENV["SKIP"] && ENV["SKIP"].include?("pages")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Pages.new.dump Backup::Pages.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring pages ... ".blue $progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore Backup::Pages.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
......
...@@ -326,7 +326,7 @@ describe Projects::IssuesController do ...@@ -326,7 +326,7 @@ describe Projects::IssuesController do
end end
describe 'POST #create' do describe 'POST #create' do
def post_new_issue(attrs = {}) def post_new_issue(issue_attrs = {}, additional_params = {})
sign_in(user) sign_in(user)
project = create(:empty_project, :public) project = create(:empty_project, :public)
project.team << [user, :developer] project.team << [user, :developer]
...@@ -334,8 +334,8 @@ describe Projects::IssuesController do ...@@ -334,8 +334,8 @@ describe Projects::IssuesController do
post :create, { post :create, {
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
issue: { title: 'Title', description: 'Description' }.merge(attrs) issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
} }.merge(additional_params)
project.issues.first project.issues.first
end end
...@@ -378,24 +378,81 @@ describe Projects::IssuesController do ...@@ -378,24 +378,81 @@ describe Projects::IssuesController do
context 'Akismet is enabled' do context 'Akismet is enabled' do
before do before do
stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end end
context 'when an issue is not identified as a spam' do
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
end
it 'does not create an issue' do
expect { post_new_issue(title: '') }.not_to change(Issue, :count)
end
end
context 'when an issue is identified as a spam' do
before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
context 'when captcha is not verified' do
def post_spam_issue def post_spam_issue
post_new_issue(title: 'Spam Title', description: 'Spam lives here') post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end end
it 'rejects an issue recognized as spam' do before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
expect{ post_spam_issue }.not_to change(Issue, :count)
expect(response).to render_template(:new) it 'rejects an issue recognized as a spam' do
expect { post_spam_issue }.not_to change(Issue, :count)
end end
it 'creates a spam log' do it 'creates a spam log' do
post_spam_issue post_spam_issue
spam_logs = SpamLog.all spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1) expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('Spam Title') expect(spam_logs.first.title).to eq('Spam Title')
expect(spam_logs.first.recaptcha_verified).to be_falsey
end
it 'does not create an issue when it is not valid' do
expect { post_new_issue(title: '') }.not_to change(Issue, :count)
end
it 'does not create an issue when recaptcha is not enabled' do
stub_application_setting(recaptcha_enabled: false)
expect { post_spam_issue }.not_to change(Issue, :count)
end
end
context 'when captcha is verified' do
let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
def post_verified_issue
post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
end
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
end
it 'accepts an issue after recaptcha is verified' do
expect { post_verified_issue }.to change(Issue, :count)
end
it 'marks spam log as recaptcha_verified' do
expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
end
it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
spam_log = create(:spam_log)
expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }.
not_to change { SpamLog.last.recaptcha_verified }
end
end
end end
end end
...@@ -405,7 +462,7 @@ describe Projects::IssuesController do ...@@ -405,7 +462,7 @@ describe Projects::IssuesController do
end end
it 'creates a user agent detail' do it 'creates a user agent detail' do
expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
end end
end end
......
...@@ -44,7 +44,7 @@ describe RegistrationsController do ...@@ -44,7 +44,7 @@ describe RegistrationsController do
post(:create, user_params) post(:create, user_params)
expect(response).to render_template(:new) expect(response).to render_template(:new)
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end end
it 'redirects to the dashboard when the recaptcha is solved' do it 'redirects to the dashboard when the recaptcha is solved' do
......
...@@ -4,6 +4,6 @@ FactoryGirl.define do ...@@ -4,6 +4,6 @@ FactoryGirl.define do
factory :timelog do factory :timelog do
time_spent 3600 time_spent 3600
user user
association :trackable, factory: :issue issue
end end
end end
...@@ -64,10 +64,6 @@ feature 'Environment', :feature do ...@@ -64,10 +64,6 @@ feature 'Environment', :feature do
expect(page).to have_link('Re-deploy') expect(page).to have_link('Re-deploy')
end end
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
scenario 'does not show terminal button' do scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button expect(page).not_to have_terminal_button
end end
...@@ -116,6 +112,7 @@ feature 'Environment', :feature do ...@@ -116,6 +112,7 @@ feature 'Environment', :feature do
end end
end end
context 'when environment is available' do
context 'with stop action' do context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
...@@ -138,6 +135,21 @@ feature 'Environment', :feature do ...@@ -138,6 +135,21 @@ feature 'Environment', :feature do
end end
end end
end end
context 'without stop action' do
scenario 'does allow to stop environment' do
click_link('Stop')
end
end
end
context 'when environment is stopped' do
given(:environment) { create(:environment, project: project, state: :stopped) }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
end
end end
end end
end end
......
...@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do ...@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do
scenario 'does show no deployments' do scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet') expect(page).to have_content('No deployments yet')
end end
context 'for available environment' do
given(:environment) { create(:environment, project: project, state: :available) }
scenario 'does not shows stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
context 'for stopped environment' do
given(:environment) { create(:environment, project: project, state: :stopped) }
scenario 'does not shows stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
end end
context 'with deployments' do context 'with deployments' do
......
require 'rails_helper'
describe 'New issue', feature: true do
include StubENV
let(:project) { create(:project, :public) }
let(:user) { create(:user)}
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
current_application_settings.update!(
akismet_enabled: true,
akismet_api_key: 'testkey',
recaptcha_enabled: true,
recaptcha_site_key: 'test site key',
recaptcha_private_key: 'test private key'
)
project.team << [user, :master]
login_as(user)
end
context 'when identified as a spam' do
before do
WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'creates an issue after solving reCaptcha' do
fill_in 'issue_title', with: 'issue title'
fill_in 'issue_description', with: 'issue description'
click_button 'Submit issue'
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title')
expect(page).to have_css('.recaptcha')
click_button 'Submit issue'
expect(page.find('.issue-details h2.title')).to have_content('issue title')
expect(page.find('.issue-details .description')).to have_content('issue description')
end
end
context 'when not identified as a spam' do
before do
WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'creates an issue' do
fill_in 'issue_title', with: 'issue title'
fill_in 'issue_description', with: 'issue description'
click_button 'Submit issue'
expect(page.find('.issue-details h2.title')).to have_content('issue title')
expect(page.find('.issue-details .description')).to have_content('issue description')
end
end
end
require 'spec_helper'
feature 'Blob shortcuts', feature: true do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
let(:sha) { project.repository.commit.sha }
describe 'On a file(blob)', js: true do
def get_absolute_url(path = "")
"http://#{page.server.host}:#{page.server.port}#{path}"
end
def visit_blob(fragment = nil)
visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
end
describe 'pressing "y"' do
it 'redirects to permalink with commit sha' do
visit_blob
find('body').native.send_key('y')
expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true)
end
it 'maintains fragment hash when redirecting' do
fragment = "L1"
visit_blob(fragment)
find('body').native.send_key('y')
expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true)
end
end
end
end
require 'spec_helper' require 'spec_helper'
feature 'Setup Mattermost slash commands', feature: true do feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service } let(:service) { project.create_mattermost_slash_commands_service }
...@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do
visit edit_namespace_project_service_path(project.namespace, project, service) visit edit_namespace_project_service_path(project.namespace, project, service)
end end
describe 'user visits the mattermost slash command config page', js: true do describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do it 'shows a help message' do
wait_for_ajax expect(page).to have_content("This service allows users to perform common")
end
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(page).to have_content("This service allows GitLab users to perform common") expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end end
it 'shows the token after saving' do it 'shows the token after saving' do
...@@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id') select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]') selected_option = select_element.find('option[selected]')
expect(select_element['disabled']).to be(true) expect(select_element['disabled']).to eq('disabled')
expect(selected_option).to have_content(team_name.to_s) expect(selected_option).to have_content(team_name.to_s)
end end
...@@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id') select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]') selected_option = select_element.find('option[selected]')
expect(select_element['disabled']).to be(false) expect(select_element['disabled']).to be(nil)
expect(selected_option).to have_content('Select team...') expect(selected_option).to have_content('Select team...')
# The 'Select team...' placeholder is item `0`. # The 'Select team...' placeholder is item `0`.
expect(select_element.all('option').count).to eq(3) expect(select_element.all('option').count).to eq(3)
...@@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end end
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
end end
end end
......
require 'spec_helper' require 'spec_helper'
feature 'Slack slash commands', feature: true do feature 'Slack slash commands', feature: true do
include WaitForAjax
given(:user) { create(:user) } given(:user) { create(:user) }
given(:project) { create(:project) } given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service } given(:service) { project.create_slack_slash_commands_service }
...@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do ...@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do background do
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
end
scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service) visit edit_namespace_project_service_path(project.namespace, project, service)
end
wait_for_ajax it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(page).to have_content('This service allows GitLab users to perform common') expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end end
scenario 'shows the token after saving' do it 'shows a help message' do
visit edit_namespace_project_service_path(project.namespace, project, service) expect(page).to have_content('This service allows users to perform common')
end
it 'shows the token after saving' do
fill_in 'service_token', with: 'token' fill_in 'service_token', with: 'token'
click_on 'Save' click_on 'Save'
...@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do ...@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token') expect(value).to eq('token')
end end
scenario 'shows the correct trigger url' do it 'shows the correct trigger url' do
visit edit_namespace_project_service_path(project.namespace, project, service)
value = find_field('url').value value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end end
......
...@@ -63,9 +63,11 @@ describe MergeRequestsHelper do ...@@ -63,9 +63,11 @@ describe MergeRequestsHelper do
end end
end end
describe 'mr_widget_refresh_url' do describe '#mr_widget_refresh_url' do
let(:project) { create(:empty_project) } let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project) } let(:project) { create(:project, :public) }
let(:project_fork) { Projects::ForkService.new(project, guest).execute }
let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
it 'returns correct url for MR' do it 'returns correct url for MR' do
expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh" expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
...@@ -74,7 +76,7 @@ describe MergeRequestsHelper do ...@@ -74,7 +76,7 @@ describe MergeRequestsHelper do
end end
it 'returns empty string for nil' do it 'returns empty string for nil' do
expect(mr_widget_refresh_url(nil)).to end_with('') expect(mr_widget_refresh_url(nil)).to eq('')
end end
end end
end end
...@@ -119,7 +119,7 @@ describe('Environment item', () => { ...@@ -119,7 +119,7 @@ describe('Environment item', () => {
}, },
], ],
}, },
'stoppable?': true, 'stop_action?': true,
environment_path: 'root/ci-folders/environments/31', environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z', updated_at: '2016-11-10T15:55:58.778Z',
......
...@@ -50,7 +50,7 @@ const environmentsList = [ ...@@ -50,7 +50,7 @@ const environmentsList = [
}, },
manual_actions: [], manual_actions: [],
}, },
'stoppable?': true, 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z',
...@@ -105,7 +105,7 @@ const environmentsList = [ ...@@ -105,7 +105,7 @@ const environmentsList = [
}, },
manual_actions: [], manual_actions: [],
}, },
'stoppable?': false, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z',
...@@ -116,7 +116,7 @@ const environmentsList = [ ...@@ -116,7 +116,7 @@ const environmentsList = [
state: 'available', state: 'available',
environment_type: 'review', environment_type: 'review',
last_deployment: null, last_deployment: null,
'stoppable?': true, 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z',
...@@ -127,7 +127,7 @@ const environmentsList = [ ...@@ -127,7 +127,7 @@ const environmentsList = [
state: 'available', state: 'available',
environment_type: 'review', environment_type: 'review',
last_deployment: null, last_deployment: null,
'stoppable?': true, 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z',
...@@ -143,7 +143,7 @@ const environment = { ...@@ -143,7 +143,7 @@ const environment = {
external_url: 'http://production.', external_url: 'http://production.',
environment_type: null, environment_type: null,
last_deployment: {}, last_deployment: {},
'stoppable?': false, 'stop_action?': false,
environment_path: '/root/review-app/environments/4', environment_path: '/root/review-app/environments/4',
stop_path: '/root/review-app/environments/4/stop', stop_path: '/root/review-app/environments/4/stop',
created_at: '2016-12-16T11:51:04.690Z', created_at: '2016-12-16T11:51:04.690Z',
......
...@@ -41,6 +41,19 @@ require('~/lib/utils/common_utils'); ...@@ -41,6 +41,19 @@ require('~/lib/utils/common_utils');
}); });
}); });
describe('gl.utils.handleLocationHash', () => {
beforeEach(() => {
window.history.pushState({}, null, '#definição');
});
it('decodes hash parameter', () => {
spyOn(window.document, 'getElementById').and.callThrough();
gl.utils.handleLocationHash();
expect(window.document.getElementById).toHaveBeenCalledWith('definição');
expect(window.document.getElementById).toHaveBeenCalledWith('user-content-definição');
});
});
describe('gl.utils.getParameterByName', () => { describe('gl.utils.getParameterByName', () => {
beforeEach(() => { beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2'); window.history.pushState({}, null, '?scope=all&p=2');
...@@ -73,5 +86,37 @@ require('~/lib/utils/common_utils'); ...@@ -73,5 +86,37 @@ require('~/lib/utils/common_utils');
expect(normalized[NGINX].nginx).toBe('ok'); expect(normalized[NGINX].nginx).toBe('ok');
}); });
}); });
describe('gl.utils.isMetaClick', () => {
it('should identify meta click on Windows/Linux', () => {
const e = {
metaKey: false,
ctrlKey: true,
which: 1,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
it('should identify meta click on macOS', () => {
const e = {
metaKey: true,
ctrlKey: false,
which: 1,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
it('should identify as meta click on middle-click or Mouse-wheel click', () => {
const e = {
metaKey: false,
ctrlKey: false,
which: 2,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
});
}); });
})(); })();
...@@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo'); ...@@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo');
expect($('#diffs')).toHaveClass('active'); expect($('#diffs')).toHaveClass('active');
}); });
}); });
describe('#opensInNewTab', function () {
var commitsLink;
var tabUrl;
beforeEach(function () {
commitsLink = '.commits-tab li a';
tabUrl = $(commitsLink).attr('href');
spyOn($.fn, 'attr').and.returnValue(tabUrl);
});
it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: false,
ctrlKey: true,
which: 1,
stopImmediatePropagation: function () {}
});
});
it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: true,
ctrlKey: false,
which: 1,
stopImmediatePropagation: function () {}
});
});
it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: false,
ctrlKey: false,
which: 2,
stopImmediatePropagation: function () {}
});
});
});
describe('#setCurrentAction', function () { describe('#setCurrentAction', function () {
beforeEach(function () { beforeEach(function () {
......
...@@ -221,5 +221,6 @@ award_emoji: ...@@ -221,5 +221,6 @@ award_emoji:
priorities: priorities:
- label - label
timelogs: timelogs:
- trackable - issue
- merge_request
- user - user
...@@ -357,8 +357,8 @@ LabelPriority: ...@@ -357,8 +357,8 @@ LabelPriority:
Timelog: Timelog:
- id - id
- time_spent - time_spent
- trackable_id - merge_request_id
- trackable_type - issue_id
- user_id - user_id
- created_at - created_at
- updated_at - updated_at
...@@ -77,8 +77,8 @@ describe Deployment, models: true do ...@@ -77,8 +77,8 @@ describe Deployment, models: true do
end end
end end
describe '#stoppable?' do describe '#stop_action?' do
subject { deployment.stoppable? } subject { deployment.stop_action? }
context 'when no other actions' do context 'when no other actions' do
let(:deployment) { build(:deployment) } let(:deployment) { build(:deployment) }
......
...@@ -112,8 +112,8 @@ describe Environment, models: true do ...@@ -112,8 +112,8 @@ describe Environment, models: true do
end end
end end
describe '#stoppable?' do describe '#stop_action?' do
subject { environment.stoppable? } subject { environment.stop_action? }
context 'when no other actions' do context 'when no other actions' do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
...@@ -142,17 +142,39 @@ describe Environment, models: true do ...@@ -142,17 +142,39 @@ describe Environment, models: true do
end end
end end
describe '#stop!' do describe '#stop_with_action!' do
let(:user) { create(:user) } let(:user) { create(:user) }
subject { environment.stop!(user) } subject { environment.stop_with_action!(user) }
before do before do
expect(environment).to receive(:stoppable?).and_call_original expect(environment).to receive(:available?).and_call_original
end end
context 'when no other actions' do context 'when no other actions' do
it { is_expected.to be_nil } context 'environment is available' do
before do
environment.update(state: :available)
end
it do
subject
expect(environment).to be_stopped
end
end
context 'environment is already stopped' do
before do
environment.update(state: :stopped)
end
it do
subject
expect(environment).to be_stopped
end
end
end end
context 'when matching action is defined' do context 'when matching action is defined' do
......
...@@ -330,4 +330,17 @@ describe Group, models: true do ...@@ -330,4 +330,17 @@ describe Group, models: true do
expect(group.members_with_parents).to include(master) expect(group.members_with_parents).to include(master)
end end
end end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
master = create(:user)
developer = create(:user)
group.add_user(master, GroupMember::MASTER)
group.add_user(developer, GroupMember::DEVELOPER)
expect(group.user_ids_for_project_authorizations).
to include(master.id, developer.id)
end
end
end end
...@@ -230,4 +230,11 @@ describe Namespace, models: true do ...@@ -230,4 +230,11 @@ describe Namespace, models: true do
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group]) expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end end
end end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
expect(namespace.user_ids_for_project_authorizations).
to eq([namespace.owner_id])
end
end
end end
...@@ -2,9 +2,37 @@ require 'rails_helper' ...@@ -2,9 +2,37 @@ require 'rails_helper'
RSpec.describe Timelog, type: :model do RSpec.describe Timelog, type: :model do
subject { build(:timelog) } subject { build(:timelog) }
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
it { is_expected.to be_valid } it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) } it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:user) }
describe 'Issuable validation' do
it 'is invalid if issue_id and merge_request_id are missing' do
subject.attributes = { issue: nil, merge_request: nil }
expect(subject).to be_invalid
end
it 'is invalid if issue_id and merge_request_id are set' do
subject.attributes = { issue: issue, merge_request: merge_request }
expect(subject).to be_invalid
end
it 'is valid if only issue_id is set' do
subject.attributes = { issue: issue, merge_request: nil }
expect(subject).to be_valid
end
it 'is valid if only merge_request_id is set' do
subject.attributes = { merge_request: merge_request, issue: nil }
expect(subject).to be_valid
end
end
end end
require 'spec_helper'
describe API::V3::DeployKeys, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
let(:project2) { create(:empty_project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
end
describe 'GET /deploy_keys' do
context 'when unauthenticated' do
it 'should return authentication error' do
get v3_api('/deploy_keys')
expect(response.status).to eq(401)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 403 error' do
get v3_api('/deploy_keys', user)
expect(response.status).to eq(403)
end
end
context 'when authenticated as admin' do
it 'should return all deploy keys' do
get v3_api('/deploy_keys', admin)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
end
end
%w(deploy_keys keys).each do |path|
describe "GET /projects/:id/#{path}" do
before { deploy_key }
it 'should return array of ssh keys' do
get v3_api("/projects/#{project.id}/#{path}", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
end
describe "GET /projects/:id/#{path}/:key_id" do
it 'should return a single key' do
get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'should return 404 Not Found with invalid ID' do
get v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/deploy_keys" do
it 'should not create an invalid ssh key' do
post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
key_attrs = attributes_for :another_key
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
end.to change{ project.deploy_keys.count }.by(1)
end
it 'returns an existing ssh key when attempting to add a duplicate' do
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
expect(response).to have_http_status(201)
end
it 'joins an existing ssh key to a new project' do
expect do
post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
expect(response).to have_http_status(201)
end
end
describe "DELETE /projects/:id/#{path}/:key_id" do
before { deploy_key }
it 'should delete existing key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
end.to change{ project.deploy_keys.count }.by(-1)
end
it 'should return 404 Not Found with invalid ID' do
delete v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/#{path}/:key_id/enable" do
let(:project2) { create(:empty_project) }
context 'when the user can admin the project' do
it 'enables the key' do
expect do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
end.to change { project2.deploy_keys.count }.from(0).to(1)
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
expect(response).to have_http_status(404)
end
end
end
describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
context 'when the user can admin the project' do
it 'disables the key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
end.to change { project.deploy_keys.count }.from(1).to(0)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
expect(response).to have_http_status(404)
end
end
end
end
end
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