Commit 7ca57c59 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into webpack

* master: (63 commits)
  Use `add_$role` helper in snippets specs
  removes old css class from everywhere
  Fixes broken build: Use jquery to get the element position in the page
  Check public snippets for spam
  Keep snippet visibility on error
  Update pipeline and commit URL and text on CI status change
  Support non-ASCII characters in GFM autocomplete
  Active tense test coverage
  Fix filtered search manager spec teaspoon error
  Reduce the number of loops that Cycle Analytics specs use
  Remove unnecessary returns / unset variables from the CoffeeScript -> JS conversion.
  update spec
  Change the reply shortcut to focus the field even without a selection.
  use destroy_all
  Remove settings cog from within admin scroll tabs; keep links centered
  add changelog
  remove old project members from project
  add spec replicating validation error
  Improve styling of the new issue message
  Don't capitalize environment name in show page
  ...
parents 5a099315 4615d099
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.16.3 (2017-01-27)
- Add caching of droplab ajax requests. !8725
- Fix access to the wiki code via HTTP when repository feature disabled. !8758
- Revert 3f17f29a. !8785
- Fix race conditions for AuthorizedProjectsWorker.
- Fix autocomplete initial undefined state.
- Fix Error 500 when repositories contain annotated tags pointing to blobs.
- Fix /explore sorting.
- Fixed label dropdown toggle text not correctly updating.
## 8.16.2 (2017-01-25) ## 8.16.2 (2017-01-25)
- allow issue filter bar to be operated with mouse only. !8681 - allow issue filter bar to be operated with mouse only. !8681
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */ /* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) { (function(w) {
$(function() { $(function() {
var toggleContainer = function(container, /* optional */toggleState) {
var $container = $(container);
$container
.find('.js-toggle-button .fa')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
$container
.find('.js-toggle-content')
.toggle(toggleState);
};
// Toggle button. Show/hide content inside parent container. // Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style. // Button does not change visibility. If button has icon - it changes chevron style.
// //
...@@ -10,14 +23,7 @@ ...@@ -10,14 +23,7 @@
// //
$('body').on('click', '.js-toggle-button', function(e) { $('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault(); e.preventDefault();
$(this) toggleContainer($(this).closest('.js-toggle-container'));
.find('.fa')
.toggleClass('fa-chevron-down fa-chevron-up')
.end()
.closest('.js-toggle-container')
.find('.js-toggle-content')
.toggle()
;
}); });
// If we're accessing a permalink, ensure it is not inside a // If we're accessing a permalink, ensure it is not inside a
...@@ -26,8 +32,8 @@ ...@@ -26,8 +32,8 @@
var anchor = hash && document.getElementById(hash); var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container'); var container = anchor && $(anchor).closest('.js-toggle-container');
if (container && container.find('.js-toggle-content').is(':hidden')) { if (container) {
container.find('.js-toggle-button').trigger('click'); toggleContainer(container, true);
anchor.scrollIntoView(); anchor.scrollIntoView();
} }
}); });
......
...@@ -182,7 +182,7 @@ require('./environment_item'); ...@@ -182,7 +182,7 @@ require('./environment_item');
<th class="environments-deploy">Last deployment</th> <th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th> <th class="environments-build">Build</th>
<th class="environments-commit">Commit</th> <th class="environments-commit">Commit</th>
<th class="environments-date">Created</th> <th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th> <th class="hidden-xs environments-actions"></th>
</tr> </tr>
</thead> </thead>
......
...@@ -39,8 +39,15 @@ require('./filtered_search_dropdown'); ...@@ -39,8 +39,15 @@ require('./filtered_search_dropdown');
getSearchInput() { getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input); const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
let value = lastToken.value || '';
return lastToken.value || ''; // Removes the first character if it is a quotation so that we can search
// with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
}
return value;
} }
init() { init() {
......
...@@ -83,12 +83,12 @@ ...@@ -83,12 +83,12 @@
_a = decodeURI("%C3%80"); _a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF"); _y = decodeURI("%C3%BF");
regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
match = regexp.exec(subtext); match = regexp.exec(subtext);
if (match) { if (match) {
return match[2] || match[1]; return (match[1] || match[1] === "") ? match[1] : match[2];
} else { } else {
return null; return null;
} }
......
...@@ -162,6 +162,7 @@ ...@@ -162,6 +162,7 @@
w.gl.utils.getSelectedFragment = () => { w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents(); const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null; if (documentFragment.textContent.length === 0) return null;
......
...@@ -156,12 +156,22 @@ require('./smart_interval'); ...@@ -156,12 +156,22 @@ require('./smart_interval');
return; return;
} }
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status && (data.status != null)) { if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha ||
data.pipeline !== _this.opts.ci_pipeline) {
_this.opts.ci_status = data.status; _this.opts.ci_status = data.status;
_this.showCIStatus(data.status); _this.showCIStatus(data.status);
if (data.coverage) { if (data.coverage) {
_this.showCICoverage(data.coverage); _this.showCICoverage(data.coverage);
} }
if (data.pipeline) {
_this.opts.ci_pipeline = data.pipeline;
_this.updatePipelineUrls(data.pipeline);
}
if (data.sha) {
_this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha);
}
if (showNotification) { if (showNotification) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
...@@ -250,6 +260,16 @@ require('./smart_interval'); ...@@ -250,6 +260,16 @@ require('./smart_interval');
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
}; };
MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
const pipelineUrl = this.opts.pipeline_path;
$('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
};
MergeRequestWidget.prototype.updateCommitUrls = function(id) {
const commitsUrl = this.opts.commits_path;
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
};
return MergeRequestWidget; return MergeRequestWidget;
})(); })();
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -39,17 +39,20 @@ require('./shortcuts_navigation'); ...@@ -39,17 +39,20 @@ require('./shortcuts_navigation');
} }
ShortcutsIssuable.prototype.replyWithSelectedText = function() { ShortcutsIssuable.prototype.replyWithSelectedText = function() {
var quote, replyField, documentFragment, selected, separator; var quote, documentFragment, selected, separator;
var replyField = $('.js-main-target-form #note_note');
documentFragment = window.gl.utils.getSelectedFragment(); documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) {
replyField.focus();
return;
}
// If the documentFragment contains more than just Markdown, don't copy as GFM. // If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return; if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") { if (selected.trim() === "") {
return; return;
} }
......
...@@ -330,10 +330,6 @@ ...@@ -330,10 +330,6 @@
} }
} }
.btn-file-option {
background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
}
.btn-build { .btn-build {
margin-left: 10px; margin-left: 10px;
......
...@@ -58,3 +58,9 @@ ...@@ -58,3 +58,9 @@
fill: $gl-text-color; fill: $gl-text-color;
} }
} }
.icon-link {
&:hover {
text-decoration: none;
}
}
...@@ -294,16 +294,18 @@ ...@@ -294,16 +294,18 @@
.container-fluid { .container-fluid {
position: relative; position: relative;
.nav-control {
@media (max-width: $screen-sm-max) {
margin-right: 75px;
}
}
} }
.controls { .controls {
float: right; float: right;
padding: 7px 0 0; padding: 7px 0 0;
@media (max-width: $screen-sm-max) {
display: none;
}
i { i {
color: $layout-link-gray; color: $layout-link-gray;
} }
...@@ -361,6 +363,7 @@ ...@@ -361,6 +363,7 @@
.fade-left { .fade-left {
@include fade(right, $gray-light); @include fade(right, $gray-light);
left: -5px; left: -5px;
text-align: center;
.fa { .fa {
left: -7px; left: -7px;
......
...@@ -7,7 +7,7 @@ module SpammableActions ...@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam def mark_as_spam
if SpamService.new(spammable).mark_as_spam! if SpamService.new(spammable).mark_as_spam!
redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully."
else else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end end
......
...@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController ...@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else else
@group.reset_path! @group.restore_path!
render action: "edit" render action: "edit"
end end
......
...@@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
title: merge_request.title, title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status, status: status,
coverage: coverage coverage: coverage,
pipeline: pipeline.try(:id)
} }
render json: response render json: response
......
class Projects::SnippetsController < Projects::ApplicationController class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions
before_action :module_enabled before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
# Allow read any snippet # Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index] before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
...@@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def create def create
@snippet = CreateSnippetService.new(@project, current_user, create_params = snippet_params.merge(request: request)
snippet_params).execute @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid? if @snippet.valid?
respond_with(@snippet, respond_with(@snippet,
...@@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id]) @snippet ||= @project.snippets.find(params[:id])
end end
alias_method :awardable, :snippet alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_project_snippet! def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet) return render_404 unless can?(current_user, :read_project_snippet, @snippet)
......
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
...@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController ...@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController
end end
def create def create
@snippet = CreateSnippetService.new(nil, current_user, create_params = snippet_params.merge(request: request)
snippet_params).execute @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet) respond_with @snippet.becomes(Snippet)
end end
...@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController ...@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController
end end
end end
alias_method :awardable, :snippet alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_snippet! def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
......
...@@ -21,7 +21,7 @@ module BlobHelper ...@@ -21,7 +21,7 @@ module BlobHelper
options[:link_opts]) options[:link_opts])
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref) elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-sm' link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project) elsif can?(current_user, :fork_project, project)
...@@ -32,7 +32,7 @@ module BlobHelper ...@@ -32,7 +32,7 @@ module BlobHelper
} }
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post link_to "Edit", fork_path, class: 'btn', method: :post
end end
end end
......
...@@ -198,7 +198,7 @@ module CommitsHelper ...@@ -198,7 +198,7 @@ module CommitsHelper
link_to( link_to(
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)), tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file btn-file-option' class: 'btn view-file js-view-file'
) do ) do
raw('View file @') + content_tag(:span, commit_sha[0..6], raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id') class: 'commit-short-id')
......
...@@ -93,10 +93,6 @@ module VisibilityLevelHelper ...@@ -93,10 +93,6 @@ module VisibilityLevelHelper
current_application_settings.default_project_visibility current_application_settings.default_project_visibility
end end
def default_snippet_visibility
current_application_settings.default_snippet_visibility
end
def default_group_visibility def default_group_visibility
current_application_settings.default_group_visibility current_application_settings.default_group_visibility
end end
......
...@@ -275,29 +275,23 @@ module Ci ...@@ -275,29 +275,23 @@ module Ci
end end
def update_coverage def update_coverage
return unless project
coverage_regex = project.build_coverage_regex
return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex) coverage = extract_coverage(trace, coverage_regex)
update_attributes(coverage: coverage) if coverage.present?
if coverage.is_a? Numeric
update_attributes(coverage: coverage)
end
end end
def extract_coverage(text, regex) def extract_coverage(text, regex)
begin return unless regex
matches = text.scan(Regexp.new(regex)).last
matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present? matches = text.scan(Regexp.new(regex)).last
coverage.to_f matches = matches.last if matches.kind_of?(Array)
end coverage = matches.gsub(/\d+(\.\d+)?/).first
rescue
# if bad regex or something goes wrong we dont want to interrupt transition if coverage.present?
# so we just silentrly ignore error for now coverage.to_f
end end
rescue
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
end end
def has_trace_file? def has_trace_file?
...@@ -522,6 +516,10 @@ module Ci ...@@ -522,6 +516,10 @@ module Ci
self.update(artifacts_expire_at: nil) self.update(artifacts_expire_at: nil)
end end
def coverage_regex
super || project.try(:build_coverage_regex)
end
def when def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end end
......
...@@ -34,7 +34,13 @@ module Spammable ...@@ -34,7 +34,13 @@ module Spammable
end end
def check_for_spam def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? if spam?
self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
end
end
def spammable_entity_type
self.class.name.underscore
end end
def spam_title def spam_title
......
...@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service ...@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
return unless valid_token?(params[:token]) return unless valid_token?(params[:token])
user = find_chat_user(params) user = find_chat_user(params)
unless user
if user
Gitlab::ChatCommands::Command.new(project, user, params).execute
else
url = authorize_chat_name_url(params) url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url) Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end end
private private
...@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service ...@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params) def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute ChatNames::AuthorizeUserService.new(self, params).execute
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
...@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet ...@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet
participant :author participant :author
participant :notes_with_associations participant :notes_with_associations
def check_for_spam?
super && project.public?
end
end end
...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable include Sortable
include Awardable include Awardable
include Mentionable include Mentionable
include Spammable
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content cache_markdown_field :content
...@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base ...@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed? default_content_html_invalidator || file_name_changed?
end end
default_value_for :visibility_level, Snippet::PRIVATE default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
belongs_to :project belongs_to :project
...@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base ...@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base
participant :author participant :author
participant :notes_with_associations participant :notes_with_associations
attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true
def self.reference_prefix def self.reference_prefix
'$' '$'
end end
...@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base ...@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base
notes.includes(:author) notes.includes(:author)
end end
def check_for_spam?
public?
end
def spammable_entity_type
'snippet'
end
class << self class << self
# Searches for snippets with a matching title or file name. # Searches for snippets with a matching title or file name.
# #
......
class CreateSnippetService < BaseService class CreateSnippetService < BaseService
def execute def execute
request = params.delete(:request)
api = params.delete(:api)
snippet = if project snippet = if project
project.snippets.build(params) project.snippets.build(params)
else else
...@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService ...@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService
end end
snippet.author = current_user snippet.author = current_user
snippet.spam = SpamService.new(snippet, request).check(api)
if snippet.save
UserAgentDetailService.new(snippet, request).create
end
snippet.save
snippet snippet
end end
end end
= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
= render 'layouts/nav/admin_settings'
.fade-left .fade-left
= icon('angle-left') = icon('angle-left')
.fade-right .fade-right
......
...@@ -63,9 +63,10 @@ ...@@ -63,9 +63,10 @@
- if @commit.status - if @commit.status
.well-segment.pipeline-info .well-segment.pipeline-info
%div{ class: "icon-container ci-status-icon-#{@commit.status}" } %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
= ci_icon_for_status(@commit.status) = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
= ci_icon_for_status(@commit.status)
Pipeline Pipeline
= link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
for for
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
%span.ci-status-label %span.ci-status-label
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- unless diff_file.submodule? - unless diff_file.submodule?
.file-actions.hidden-xs .file-actions.hidden-xs
- if blob_text_viewable?(blob) - if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment') = icon('comment')
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.col-md-9 .col-md-9
%h3.page-title= @environment.name.capitalize %h3.page-title= @environment.name
.col-md-3 .col-md-3
.nav-controls .nav-controls
= render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
%th ID %th ID
%th Commit %th Commit
%th Build %th Build
%th %th Created
%th.hidden-xs %th.hidden-xs
= render @deployments = render @deployments
......
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')", '@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' } type: 'button' }
Edit inline Edit inline
%a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } %a.btn.view-file{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}} View file @{{conflictsData.shortCommitSha}}
...@@ -2,14 +2,15 @@ ...@@ -2,14 +2,15 @@
.mr-widget-heading .mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status| - %w[success success_with_warnings skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status) = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status)
%span %span
Pipeline Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status) = ci_label_for_status(status)
for for
= succeed "." do = succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage %span.ci-coverage
- elsif @merge_request.has_ci? - elsif @merge_request.has_ci?
......
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
preparing: "{{status}} build", preparing: "{{status}} build",
normal: "Build {{status}}" normal: "Build {{status}}"
}, },
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
commits_path: "#{project_commits_path(@project)}",
pipeline_path: "#{project_pipelines_path(@project)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
}; };
......
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
.info-well .info-well
- if @commit.status - if @commit.status
.well-segment.pipeline-info .well-segment.pipeline-info
%div{ class: "icon-container ci-status-icon-#{@commit.status}" } .icon-container
= ci_icon_for_status(@commit.status) = icon('clock-o')
= pluralize @pipeline.statuses.count(:id), "build" = pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref - if @pipeline.ref
from from
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet New snippet
- if @snippet.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
...@@ -27,3 +29,6 @@ ...@@ -27,3 +29,6 @@
%li %li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit Edit
- if @snippet.submittable_as_spam? && current_user.admin?
%li
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
%h3.page-title %h3.page-title
Edit Snippet Edit Snippet
%hr %hr
= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level = render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet)
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
%h3.page-title %h3.page-title
New Snippet New Snippet
%hr %hr
= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility = render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet)
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.col-sm-10 .col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true = f.text_field :title, class: 'form-control', required: true, autofocus: true
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor .file-editor
.form-group .form-group
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
- if current_user - if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
New snippet New snippet
- if @snippet.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if current_user - if current_user
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
...@@ -26,3 +28,6 @@ ...@@ -26,3 +28,6 @@
%li %li
= link_to edit_snippet_path(@snippet) do = link_to edit_snippet_path(@snippet) do
Edit Edit
- if @snippet.submittable_as_spam? && current_user.admin?
%li
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
%h3.page-title %h3.page-title
Edit Snippet Edit Snippet
%hr %hr
= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level = render 'shared/snippets/form', url: snippet_path(@snippet)
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
%h3.page-title %h3.page-title
New Snippet New Snippet
%hr %hr
= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility = render "shared/snippets/form", url: snippets_path(@snippet)
--- ---
title: Fix autocomplete initial undefined state title: 19164 Add settings dropdown to mobile screens
merge_request: merge_request:
author: author:
---
title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
merge_request: 8752
author:
---
title: Update pipeline and commit links when CI status is updated
merge_request: 8351
author:
---
title: Add caching of droplab ajax requests
merge_request: 8725
author:
--- ---
title: Fix race conditions for AuthorizedProjectsWorker title: Improve pipeline status icon linking in widgets
merge_request: merge_request:
author: author:
---
title: Support non-ASCII characters in GFM autocomplete
merge_request: 8729
author:
--- ---
title: Fixed label dropdown toggle text not correctly updating title: Fix permalink discussion note being collapsed
merge_request: merge_request:
author: author:
---
title: Unify MR diff file button style
merge_request: 8874
author:
---
title: Don't capitalize environment name in show page
merge_request:
author:
---
title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
merge_request:
author:
---
title: Change the reply shortcut to focus the field even without a selection.
merge_request: 8873
author: Brian Hall
---
title: Fix access to the wiki code via HTTP when repository feature disabled
merge_request: 8758
author:
---
title: resolve deprecation warnings
merge_request: 8855
author: Adam Pahlevi
---
title: Fix filtering usernames with multiple words
merge_request: 8851
author:
---
title: Remove old project members when retrying an export
merge_request:
author:
---
title: Add ability to define a coverage regex in the .gitlab-ci.yml
merge_request: 7447
author: Leandro Camargo
---
title: Revert 3f17f29a
merge_request: 8785
author:
---
title: Fix Error 500 when repositories contain annotated tags pointing to blobs
merge_request:
author:
--- ---
title: Fix /explore sorting title: Check public snippets for spam
merge_request: merge_request:
author: author:
---
title: Reformat messages ChatOps
merge_request: 8528
author:
...@@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do member do
get 'raw' get 'raw'
post :mark_as_spam
end end
end end
......
...@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do ...@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do
member do member do
get 'raw' get 'raw'
get 'download' get 'download'
post :mark_as_spam
end end
end end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddCoverageRegexToBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :ci_builds, :coverage_regex, :string
end
end
...@@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do ...@@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "queued_at" t.datetime "queued_at"
t.string "token" t.string "token"
t.integer "lock_version" t.integer "lock_version"
t.string "coverage_regex"
end end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
......
...@@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: ...@@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| after_script | no | Define commands that run after each job's script | | after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables | | variables | no | Define build variables |
| cache | no | Define list of files that should be cached between subsequent runs | | cache | no | Define list of files that should be cached between subsequent runs |
| coverage | no | Define coverage settings for all jobs |
### image and services ### image and services
...@@ -278,6 +279,23 @@ cache: ...@@ -278,6 +279,23 @@ cache:
untracked: true untracked: true
``` ```
### coverage
`coverage` allows you to configure how coverage will be filtered out from the
build outputs. Setting this up globally will make all the jobs to use this
setting for output filtering and extracting the coverage information from your
builds.
Regular expressions are the only valid kind of value expected here. So, using
surrounding `/` is mandatory in order to consistently and explicitly represent
a regular expression string. You must escape special characters if you want to
match them literally.
A simple example:
```yaml
coverage: /\(\d+\.\d+\) covered\./
```
## Jobs ## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
...@@ -319,6 +337,7 @@ job_name: ...@@ -319,6 +337,7 @@ job_name:
| before_script | no | Override a set of commands that are executed before build | | before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build | | after_script | no | Override a set of commands that are executed after build |
| environment | no | Defines a name of environment to which deployment is done by this build | | environment | no | Defines a name of environment to which deployment is done by this build |
| coverage | no | Define coverage settings for a given job |
### script ### script
...@@ -993,6 +1012,25 @@ job: ...@@ -993,6 +1012,25 @@ job:
- execute this after my script - execute this after my script
``` ```
### job coverage
This entry is pretty much the same as described in the global context in
[`coverage`](#coverage). The only difference is that, by setting it inside
the job level, whatever is set in there will take precedence over what has
been defined in the global level. A quick example of one overriding the
other would be:
```yaml
coverage: /\(\d+\.\d+\) covered\./
job1:
coverage: /Code coverage: \d+\.\d+/
```
In the example above, considering the context of the job `job1`, the coverage
regex that would be used is `/Code coverage: \d+\.\d+/` instead of
`/\(\d+\.\d+\) covered\./`.
## Git Strategy ## Git Strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed > Introduced in GitLab 8.9 as an experimental feature. May change or be removed
......
...@@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net]( ...@@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net](
### Hover ### Hover
Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect.
View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
......
@dashboard
Feature: Dashboard Shortcuts
Background:
Given I sign in as a user
And I visit dashboard page
@javascript
Scenario: Navigate to projects tab
Given I press "g" and "p"
Then the active main tab should be Projects
@javascript
Scenario: Navigate to issue tab
Given I press "g" and "i"
Then the active main tab should be Issues
@javascript
Scenario: Navigate to merge requests tab
Given I press "g" and "m"
Then the active main tab should be Merge Requests
class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedSidebarActiveTab
include SharedShortcuts
end
...@@ -58,7 +58,7 @@ module API ...@@ -58,7 +58,7 @@ module API
end end
post ":id/snippets" do post ":id/snippets" do
authorize! :create_project_snippet, user_project authorize! :create_project_snippet, user_project
snippet_params = declared_params snippet_params = declared_params.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code) snippet_params[:content] = snippet_params.delete(:code)
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
......
...@@ -64,7 +64,7 @@ module API ...@@ -64,7 +64,7 @@ module API
desc: 'The visibility level of the snippet' desc: 'The visibility level of the snippet'
end end
post do post do
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted? if snippet.persisted?
......
...@@ -61,6 +61,7 @@ module Ci ...@@ -61,6 +61,7 @@ module Ci
allow_failure: job[:allow_failure] || false, allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success', when: job[:when] || 'on_success',
environment: job[:environment_name], environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
options: { options: {
image: job[:image], image: job[:image],
......
...@@ -10,13 +10,16 @@ module Gitlab ...@@ -10,13 +10,16 @@ module Gitlab
def find_for_git_client(login, password, project:, ip:) def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil? raise "Must provide an IP for rate limiting" if ip.nil?
# `user_with_password_for_git` should be the last check
# because it's the most expensive, especially when LDAP
# is enabled.
result = result =
service_request_check(login, password, project) || service_request_check(login, password, project) ||
build_access_token_check(login, password) || build_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) ||
lfs_token_check(login, password) || lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) || personal_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login) rate_limit!(ip, success: result.success?, login: login)
...@@ -143,7 +146,9 @@ module Gitlab ...@@ -143,7 +146,9 @@ module Gitlab
read_authentication_abilities read_authentication_abilities
end end
Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password) if Devise.secure_compare(token_handler.token, password)
Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
end
end end
def build_access_token_check(login, password) def build_access_token_check(login, password)
......
...@@ -42,10 +42,6 @@ module Gitlab ...@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid) def find_by_iid(iid)
collection.find_by(iid: iid) collection.find_by(iid: iid)
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
end end
end end
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand class Command < BaseCommand
COMMANDS = [ COMMANDS = [
Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueCreate, Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy, Gitlab::ChatCommands::Deploy,
].freeze ].freeze
...@@ -13,51 +13,32 @@ module Gitlab ...@@ -13,51 +13,32 @@ module Gitlab
if command if command
if command.allowed?(project, current_user) if command.allowed?(project, current_user)
present command.new(project, current_user, params).execute(match) command.new(project, current_user, params).execute(match)
else else
access_denied Gitlab::ChatCommands::Presenters::Access.new.access_denied
end end
else else
help(help_messages) Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end end
end end
def match_command def match_command
match = nil match = nil
service = available_commands.find do |klass| service =
match = klass.match(command) available_commands.find do |klass|
end match = klass.match(params[:text])
end
[service, match] [service, match]
end end
private private
def help_messages
available_commands.map(&:help_message)
end
def available_commands def available_commands
COMMANDS.select do |klass| COMMANDS.select do |klass|
klass.available?(project) klass.available?(project)
end end
end end
def command
params[:text]
end
def help(messages)
presenter.help(messages, params[:command])
end
def access_denied
presenter.access_denied
end
def present(resource)
presenter.present(resource)
end
end end
end end
end end
module Gitlab module Gitlab
module ChatCommands module ChatCommands
class Deploy < BaseCommand class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end end
...@@ -24,35 +22,29 @@ module Gitlab ...@@ -24,35 +22,29 @@ module Gitlab
to = match[:to] to = match[:to]
actions = find_actions(from, to) actions = find_actions(from, to)
return unless actions.present?
if actions.one? if actions.none?
play!(from, to, actions.first) Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
elsif actions.one?
action = play!(from, to, actions.first)
Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else else
Result.new(:error, 'Too many actions defined') Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end end
end end
private private
def play!(from, to, action) def play!(from, to, action)
new_action = action.play(current_user) action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
end end
def find_actions(from, to) def find_actions(from, to)
environment = project.environments.find_by(name: from) environment = project.environments.find_by(name: from)
return unless environment return [] unless environment
environment.actions_for(to).select(&:starts_environment?) environment.actions_for(to).select(&:starts_environment?)
end end
def url(subject)
polymorphic_url(
[subject.project.namespace.becomes(Namespace), subject.project, subject]
)
end
end end
end end
end end
module Gitlab
module ChatCommands
class Help < BaseCommand
# This class has to be used last, as it always matches. It has to match
# because other commands were not triggered and we want to show the help
# command
def self.match(_text)
true
end
def self.help_message
'help'
end
def self.allowed?(_project, _user)
true
end
def execute(commands, text)
Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
end
def trigger
params[:command]
end
end
end
end
module Gitlab module Gitlab
module ChatCommands module ChatCommands
class IssueCreate < IssueCommand class IssueNew < IssueCommand
def self.match(text) def self.match(text)
# we can not match \n with the dot by passing the m modifier as than # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated # the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end end
...@@ -19,8 +19,24 @@ module Gitlab ...@@ -19,8 +19,24 @@ module Gitlab
title = match[:title] title = match[:title]
description = match[:description].to_s.rstrip description = match[:description].to_s.rstrip
issue = create_issue(title: title, description: description)
if issue.persisted?
presenter(issue).present
else
presenter(issue).display_errors
end
end
private
def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute Issues::CreateService.new(project, current_user, title: title, description: description).execute
end end
def presenter(issue)
Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
end
end end
end end
end end
...@@ -10,7 +10,13 @@ module Gitlab ...@@ -10,7 +10,13 @@ module Gitlab
end end
def execute(match) def execute(match)
collection.search(match[:query]).limit(QUERY_LIMIT) issues = collection.search(match[:query]).limit(QUERY_LIMIT)
if issues.present?
Presenters::IssueSearch.new(issues).present
else
Presenters::Access.new(issues).not_found
end
end end
end end
end end
......
...@@ -10,7 +10,13 @@ module Gitlab ...@@ -10,7 +10,13 @@ module Gitlab
end end
def execute(match) def execute(match)
find_by_iid(match[:iid]) issue = find_by_iid(match[:iid])
if issue
Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
else
Gitlab::ChatCommands::Presenters::Access.new.not_found
end
end end
end end
end end
......
module Gitlab
module ChatCommands
class Presenter
include Gitlab::Routing
def authorize_chat_name(url)
message = if url
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(message)
end
def help(commands, trigger)
if commands.none?
ephemeral_response("No commands configured")
else
commands.map! { |command| "#{trigger} #{command}" }
message = header_with_list("Available commands", commands)
ephemeral_response(message)
end
end
def present(subject)
return not_found unless subject
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
if subject.none?
not_found
elsif subject.one?
single_resource(subject.first)
else
multiple_resources(subject)
end
else
single_resource(subject)
end
end
def access_denied
ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
private
def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message)
end
end
def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted?
message = "#{title(resource)}:"
message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message)
end
def multiple_resources(resources)
titles = resources.map { |resource| title(resource) }
message = header_with_list("Multiple results were found:", titles)
ephemeral_response(message)
end
def error(resource)
message = header_with_list("The action was not successful, because:", resource.errors.messages)
ephemeral_response(message)
end
def title(resource)
reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{title}](#{url(resource)})"
end
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def url(resource)
url_for(
[
resource.project.namespace.becomes(Namespace),
resource.project,
resource
]
)
end
def ephemeral_response(message)
{
response_type: :ephemeral,
text: message,
status: 200
}
end
def in_channel_response(message)
{
response_type: :in_channel,
text: message,
status: 200
}
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class Access < Presenters::Base
def access_denied
ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
def not_found
ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def authorize
message =
if @resource
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(text: message)
end
def unknown_command(commands)
ephemeral_response(text: help_message(trigger))
end
private
def help_message(trigger)
header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
end
def full_commands(trigger)
@resource.map { |command| "#{trigger} #{command.help_message}" }
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class Base
include Gitlab::Routing.url_helpers
def initialize(resource = nil)
@resource = resource
end
def display_errors
message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
ephemeral_response(text: message)
end
private
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def ephemeral_response(message)
response = {
response_type: :ephemeral,
status: 200
}.merge(message)
format_response(response)
end
def in_channel_response(message)
response = {
response_type: :in_channel,
status: 200
}.merge(message)
format_response(response)
end
def format_response(response)
response[:text] = format(response[:text]) if response.has_key?(:text)
if response.has_key?(:attachments)
response[:attachments].each do |attachment|
attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
attachment[:text] = format(attachment[:text]) if attachment[:text]
end
end
response
end
# Convert Markdown to slacks format
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def resource_url
url_for(
[
@resource.project.namespace.becomes(Namespace),
@resource.project,
@resource
]
)
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class Deploy < Presenters::Base
def present(from, to)
message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
in_channel_response(text: message)
end
def no_actions
ephemeral_response(text: "No action found to be executed")
end
def too_many_actions
ephemeral_response(text: "Too many actions defined")
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class Help < Presenters::Base
def present(trigger, text)
ephemeral_response(text: help_message(trigger, text))
end
private
def help_message(trigger, text)
return "No commands available :thinking_face:" unless @resource.present?
if text.start_with?('help')
header_with_list("Available commands", full_commands(trigger))
else
header_with_list("Unknown command, these commands are available", full_commands(trigger))
end
end
def full_commands(trigger)
@resource.map { |command| "#{trigger} #{command.help_message}" }
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
module Issuable
def color(issuable)
issuable.open? ? '#38ae67' : '#d22852'
end
def status_text(issuable)
issuable.open? ? 'Open' : 'Closed'
end
def project
@resource.project
end
def author
@resource.author
end
def fields
[
{
title: "Assignee",
value: @resource.assignee ? @resource.assignee.name : "_None_",
short: true
},
{
title: "Milestone",
value: @resource.milestone ? @resource.milestone.title : "_None_",
short: true
},
{
title: "Labels",
value: @resource.labels.any? ? @resource.label_names : "_None_",
short: true
}
]
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class IssueNew < Presenters::Base
include Presenters::Issuable
def present
in_channel_response(new_issue)
end
private
def new_issue
{
attachments: [
{
title: "#{@resource.title} · #{@resource.to_reference}",
title_link: resource_url,
author_name: author.name,
author_icon: author.avatar_url,
fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
pretext: pretext,
color: color(@resource),
fields: fields,
mrkdwn_in: [
:title,
:pretext,
:text,
:fields
]
}
]
}
end
def pretext
"I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
end
def project_link
"[#{project.name_with_namespace}](#{projects_url(project)})"
end
def author_profile_link
"[#{author.to_reference}](#{url_for(author)})"
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class IssueSearch < Presenters::Base
include Presenters::Issuable
def present
text = if @resource.count >= 5
"Here are the first 5 issues I found:"
elsif @resource.one?
"Here is the only issue I found:"
else
"Here are the #{@resource.count} issues I found:"
end
ephemeral_response(text: text, attachments: attachments)
end
private
def attachments
@resource.map do |issue|
url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
{
color: color(issue),
fallback: "#{issue.to_reference} #{issue.title}",
text: "#{url} · #{issue.title} (#{status_text(issue)})",
mrkdwn_in: [
:text
]
}
end
end
def project
@project ||= @resource.first.project
end
def namespace
@namespace ||= project.namespace.becomes(Namespace)
end
end
end
end
end
module Gitlab
module ChatCommands
module Presenters
class IssueShow < Presenters::Base
include Presenters::Issuable
def present
if @resource.confidential?
ephemeral_response(show_issue)
else
in_channel_response(show_issue)
end
end
private
def show_issue
{
attachments: [
{
title: "#{@resource.title} · #{@resource.to_reference}",
title_link: resource_url,
author_name: author.name,
author_icon: author.avatar_url,
fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
pretext: pretext,
text: text,
color: color(@resource),
fields: fields,
mrkdwn_in: [
:pretext,
:text,
:fields
]
}
]
}
end
def text
message = "**#{status_text(@resource)}**"
if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
return message
end
message << " · "
message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
message
end
def pretext
"Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
end
end
end
end
end
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents Coverage settings.
#
class Coverage < Node
include Validatable
validations do
validates :config, regexp: true
end
def value
@config[1...-1]
end
end
end
end
end
end
...@@ -33,8 +33,11 @@ module Gitlab ...@@ -33,8 +33,11 @@ module Gitlab
entry :cache, Entry::Cache, entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.' description: 'Configure caching between build jobs.'
entry :coverage, Entry::Coverage,
description: 'Coverage configuration for this pipeline.'
helpers :before_script, :image, :services, :after_script, helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs :variables, :stages, :types, :cache, :coverage, :jobs
def compose!(_deps = nil) def compose!(_deps = nil)
super(self) do super(self) do
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script type stage when artifacts cache dependencies before_script
after_script variables environment] after_script variables environment coverage]
validations do validations do
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
...@@ -71,9 +71,12 @@ module Gitlab ...@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment, entry :environment, Entry::Environment,
description: 'Environment configuration for this job.' description: 'Environment configuration for this job.'
entry :coverage, Entry::Coverage,
description: 'Coverage configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script, helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables, :cache, :image, :services, :only, :except, :variables,
:artifacts, :commands, :environment :artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies attributes :script, :tags, :allow_failure, :when, :dependencies
...@@ -130,6 +133,7 @@ module Gitlab ...@@ -130,6 +133,7 @@ module Gitlab
variables: variables_defined? ? variables_value : nil, variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil, environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil, environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value, artifacts: artifacts_value,
after_script: after_script_value } after_script: after_script_value }
end end
......
...@@ -28,17 +28,21 @@ module Gitlab ...@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol) value.is_a?(String) || value.is_a?(Symbol)
end end
def validate_regexp(value)
!value.nil? && Regexp.new(value.to_s) && true
rescue RegexpError, TypeError
false
end
def validate_string_or_regexp(value) def validate_string_or_regexp(value)
return true if value.is_a?(Symbol) return true if value.is_a?(Symbol)
return false unless value.is_a?(String) return false unless value.is_a?(String)
if value.first == '/' && value.last == '/' if value.first == '/' && value.last == '/'
Regexp.new(value[1...-1]) validate_regexp(value[1...-1])
else else
true true
end end
rescue RegexpError
false
end end
def validate_boolean(value) def validate_boolean(value)
......
...@@ -9,15 +9,7 @@ module Gitlab ...@@ -9,15 +9,7 @@ module Gitlab
include Validatable include Validatable
validations do validations do
include LegacyValidationHelpers validates :config, array_of_strings_or_regexps: true
validate :array_of_strings_or_regexps
def array_of_strings_or_regexps
unless validate_array_of_strings_or_regexps(config)
errors.add(:config, 'should be an array of strings or regexps')
end
end
end end
end end
end end
......
...@@ -54,6 +54,51 @@ module Gitlab ...@@ -54,6 +54,51 @@ module Gitlab
end end
end end
class RegexpValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
def validate_each(record, attribute, value)
unless validate_regexp(value)
record.errors.add(attribute, 'must be a regular expression')
end
end
private
def look_like_regexp?(value)
value.is_a?(String) && value.start_with?('/') &&
value.end_with?('/')
end
def validate_regexp(value)
look_like_regexp?(value) &&
Regexp.new(value.to_s[1...-1]) &&
true
rescue RegexpError
false
end
end
class ArrayOfStringsOrRegexpsValidator < RegexpValidator
def validate_each(record, attribute, value)
unless validate_array_of_strings_or_regexps(value)
record.errors.add(attribute, 'should be an array of strings or regexps')
end
end
private
def validate_array_of_strings_or_regexps(values)
values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
end
def validate_string_or_regexp(value)
return false unless value.is_a?(String)
return validate_regexp(value) if look_like_regexp?(value)
true
end
end
class TypeValidator < ActiveModel::EachValidator class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
type = options[:with] type = options[:with]
......
...@@ -41,6 +41,8 @@ module Gitlab ...@@ -41,6 +41,8 @@ module Gitlab
end end
def ensure_default_member! def ensure_default_member!
@project.project_members.destroy_all
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end end
......
...@@ -6,8 +6,8 @@ describe Projects::SnippetsController do ...@@ -6,8 +6,8 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) } let(:user2) { create(:user) }
before do before do
project.team << [user, :master] project.add_master(user)
project.team << [user2, :master] project.add_master(user2)
end end
describe 'GET #index' do describe 'GET #index' do
...@@ -69,6 +69,86 @@ describe Projects::SnippetsController do ...@@ -69,6 +69,86 @@ describe Projects::SnippetsController do
end end
end end
describe 'POST #create' do
def create_snippet(project, snippet_params = {})
sign_in(user)
project.add_developer(user)
post :create, {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}
end
context 'when the snippet is spam' do
before do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
context 'when the project is private' do
let(:private_project) { create(:project_empty_repo, :private) }
context 'when the snippet is public' do
it 'creates the snippet' do
expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
to change { Snippet.count }.by(1)
end
end
end
context 'when the project is public' do
context 'when the snippet is private' do
it 'creates the snippet' do
expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
it 'rejects the shippet' do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
not_to change { Snippet.count }
expect(response).to render_template(:new)
end
it 'creates a spam log' do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
to change { SpamLog.count }.by(1)
end
end
end
end
end
describe 'POST #mark_as_spam' do
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
before do
allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
stub_application_setting(akismet_enabled: true)
end
def mark_as_spam
admin = create(:admin)
create(:user_agent_detail, subject: snippet)
project.add_master(admin)
sign_in(admin)
post :mark_as_spam,
namespace_id: project.namespace.path,
project_id: project.path,
id: snippet.id
end
it 'updates the snippet' do
mark_as_spam
expect(snippet.reload).not_to be_submittable_as_spam
end
end
%w[show raw].each do |action| %w[show raw].each do |action|
describe "GET ##{action}" do describe "GET ##{action}" do
context 'when the project snippet is private' do context 'when the project snippet is private' do
......
...@@ -138,6 +138,65 @@ describe SnippetsController do ...@@ -138,6 +138,65 @@ describe SnippetsController do
end end
end end
describe 'POST #create' do
def create_snippet(snippet_params = {})
sign_in(user)
post :create, {
personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
}
end
context 'when the snippet is spam' do
before do
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
context 'when the snippet is private' do
it 'creates the snippet' do
expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
it 'rejects the shippet' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
not_to change { Snippet.count }
expect(response).to render_template(:new)
end
it 'creates a spam log' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
to change { SpamLog.count }.by(1)
end
end
end
end
describe 'POST #mark_as_spam' do
let(:snippet) { create(:personal_snippet, :public, author: user) }
before do
allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
stub_application_setting(akismet_enabled: true)
end
def mark_as_spam
admin = create(:admin)
create(:user_agent_detail, subject: snippet)
sign_in(admin)
post :mark_as_spam, id: snippet.id
end
it 'updates the snippet' do
mark_as_spam
expect(snippet.reload).not_to be_submittable_as_spam
end
end
%w(raw download).each do |action| %w(raw download).each do |action|
describe "GET #{action}" do describe "GET #{action}" do
context 'when the personal snippet is private' do context 'when the personal snippet is private' do
......
require 'spec_helper'
feature 'Dashboard shortcuts', feature: true, js: true do
before do
login_as :user
visit dashboard_projects_path
end
scenario 'Navigate to tabs' do
find('body').native.send_key('g')
find('body').native.send_key('p')
ensure_active_main_tab('Projects')
find('body').native.send_key('g')
find('body').native.send_key('i')
ensure_active_main_tab('Issues')
find('body').native.send_key('g')
find('body').native.send_key('m')
ensure_active_main_tab('Merge Requests')
end
def ensure_active_main_tab(content)
expect(find('.nav-sidebar li.active')).to have_content(content)
end
end
...@@ -19,6 +19,10 @@ feature 'Environment', :feature do ...@@ -19,6 +19,10 @@ feature 'Environment', :feature do
visit_environment(environment) visit_environment(environment)
end end
scenario 'shows environment name' do
expect(page).to have_content(environment.name)
end
context 'without deployments' do context 'without deployments' do
scenario 'does show no deployments' do scenario 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.') expect(page).to have_content('You don\'t have any deployments right now.')
......
...@@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do ...@@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do
end end
scenario 'does create a new pipeline' do scenario 'does create a new pipeline' do
expect(page).to have_content('Production') expect(page).to have_content('production')
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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