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 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
- 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 */
(function(w) {
$(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.
// Button does not change visibility. If button has icon - it changes chevron style.
//
......@@ -10,14 +23,7 @@
//
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
$(this)
.find('.fa')
.toggleClass('fa-chevron-down fa-chevron-up')
.end()
.closest('.js-toggle-container')
.find('.js-toggle-content')
.toggle()
;
toggleContainer($(this).closest('.js-toggle-container'));
});
// If we're accessing a permalink, ensure it is not inside a
......@@ -26,8 +32,8 @@
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
if (container && container.find('.js-toggle-content').is(':hidden')) {
container.find('.js-toggle-button').trigger('click');
if (container) {
toggleContainer(container, true);
anchor.scrollIntoView();
}
});
......
......@@ -182,7 +182,7 @@ require('./environment_item');
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</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>
</tr>
</thead>
......
......@@ -39,8 +39,15 @@ require('./filtered_search_dropdown');
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
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() {
......
......@@ -83,12 +83,12 @@
_a = decodeURI("%C3%80");
_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);
if (match) {
return match[2] || match[1];
return (match[1] || match[1] === "") ? match[1] : match[2];
} else {
return null;
}
......
......@@ -162,6 +162,7 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null;
......
......@@ -156,12 +156,22 @@ require('./smart_interval');
return;
}
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.showCIStatus(data.status);
if (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) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
......@@ -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);
};
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;
})();
})(window.gl || (window.gl = {}));
......@@ -39,17 +39,20 @@ require('./shortcuts_navigation');
}
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();
if (!documentFragment) return;
if (!documentFragment) {
replyField.focus();
return;
}
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") {
return;
}
......
......@@ -330,10 +330,6 @@
}
}
.btn-file-option {
background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
}
.btn-build {
margin-left: 10px;
......
......@@ -58,3 +58,9 @@
fill: $gl-text-color;
}
}
.icon-link {
&:hover {
text-decoration: none;
}
}
......@@ -294,16 +294,18 @@
.container-fluid {
position: relative;
.nav-control {
@media (max-width: $screen-sm-max) {
margin-right: 75px;
}
}
}
.controls {
float: right;
padding: 7px 0 0;
@media (max-width: $screen-sm-max) {
display: none;
}
i {
color: $layout-link-gray;
}
......@@ -361,6 +363,7 @@
.fade-left {
@include fade(right, $gray-light);
left: -5px;
text-align: center;
.fa {
left: -7px;
......
......@@ -7,7 +7,7 @@ module SpammableActions
def 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
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
......
......@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
@group.reset_path!
@group.restore_path!
render action: "edit"
end
......
......@@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage
coverage: coverage,
pipeline: pipeline.try(:id)
}
render json: response
......
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
include SpammableActions
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
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
......@@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
@snippet = CreateSnippetService.new(@project, current_user,
snippet_params).execute
create_params = snippet_params.merge(request: request)
@snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid?
respond_with(@snippet,
......@@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
......
class SnippetsController < ApplicationController
include ToggleAwardEmoji
include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
......@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController
end
def create
@snippet = CreateSnippetService.new(nil, current_user,
snippet_params).execute
create_params = snippet_params.merge(request: request)
@snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet)
end
......@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController
end
end
alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
......
......@@ -21,7 +21,7 @@ module BlobHelper
options[:link_opts])
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)
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
......@@ -32,7 +32,7 @@ module BlobHelper
}
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
......
......@@ -198,7 +198,7 @@ module CommitsHelper
link_to(
namespace_project_blob_path(project.namespace, project,
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
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
......
......@@ -93,10 +93,6 @@ module VisibilityLevelHelper
current_application_settings.default_project_visibility
end
def default_snippet_visibility
current_application_settings.default_snippet_visibility
end
def default_group_visibility
current_application_settings.default_group_visibility
end
......
......@@ -275,29 +275,23 @@ module Ci
end
def update_coverage
return unless project
coverage_regex = project.build_coverage_regex
return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
if coverage.is_a? Numeric
update_attributes(coverage: coverage)
end
update_attributes(coverage: coverage) if coverage.present?
end
def extract_coverage(text, regex)
begin
matches = text.scan(Regexp.new(regex)).last
matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
return unless regex
if coverage.present?
coverage.to_f
end
rescue
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
matches = text.scan(Regexp.new(regex)).last
matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
coverage.to_f
end
rescue
# if bad regex or something goes wrong we dont want to interrupt transition
# so we just silentrly ignore error for now
end
def has_trace_file?
......@@ -522,6 +516,10 @@ module Ci
self.update(artifacts_expire_at: nil)
end
def coverage_regex
super || project.try(:build_coverage_regex)
end
def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end
......
......@@ -34,7 +34,13 @@ module Spammable
end
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
def spam_title
......
......@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
return unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
if user
Gitlab::ChatCommands::Command.new(project, user, params).execute
else
url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url)
Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end
private
......@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end
......@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
def check_for_spam?
super && project.public?
end
end
......@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable
include Awardable
include Mentionable
include Spammable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
......@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
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 :project
......@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base
participant :author
participant :notes_with_associations
attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true
def self.reference_prefix
'$'
end
......@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base
notes.includes(:author)
end
def check_for_spam?
public?
end
def spammable_entity_type
'snippet'
end
class << self
# Searches for snippets with a matching title or file name.
#
......
class CreateSnippetService < BaseService
def execute
request = params.delete(:request)
api = params.delete(:api)
snippet = if project
project.snippets.build(params)
else
......@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
snippet.spam = SpamService.new(snippet, request).check(api)
if snippet.save
UserAgentDetailService.new(snippet, request).create
end
snippet.save
snippet
end
end
= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class }
= render 'layouts/nav/admin_settings'
.fade-left
= icon('angle-left')
.fade-right
......
......@@ -63,9 +63,10 @@
- if @commit.status
.well-segment.pipeline-info
%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
= 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
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
%span.ci-status-label
......
......@@ -5,7 +5,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- 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')
\
- if editable_diff?(diff_file)
......
......@@ -5,7 +5,7 @@
%div{ class: container_class }
.top-area.adjust
.col-md-9
%h3.page-title= @environment.name.capitalize
%h3.page-title= @environment.name
.col-md-3
.nav-controls
= render 'projects/environments/terminal_button', environment: @environment
......@@ -33,7 +33,7 @@
%th ID
%th Commit
%th Build
%th
%th Created
%th.hidden-xs
= render @deployments
......
......@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
%a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" }
%a.btn.view-file{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}}
......@@ -2,14 +2,15 @@
.mr-widget-heading
- %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_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
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
= 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
- elsif @merge_request.has_ci?
......
......@@ -24,6 +24,10 @@
preparing: "{{status}} build",
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)}"
};
......
......@@ -23,8 +23,8 @@
.info-well
- if @commit.status
.well-segment.pipeline-info
%div{ class: "icon-container ci-status-icon-#{@commit.status}" }
= ci_icon_for_status(@commit.status)
.icon-container
= icon('clock-o')
= pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref
from
......
......@@ -8,6 +8,8 @@
- 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
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)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......@@ -27,3 +29,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
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 @@
%h3.page-title
Edit Snippet
%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 @@
%h3.page-title
New Snippet
%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 @@
.col-sm-10
= 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
.form-group
......
......@@ -8,6 +8,8 @@
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
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
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......@@ -26,3 +28,6 @@
%li
= link_to edit_snippet_path(@snippet) do
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 @@
%h3.page-title
Edit Snippet
%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 @@
%h3.page-title
New Snippet
%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:
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:
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:
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:
author:
---
title: Reformat messages ChatOps
merge_request: 8528
author:
......@@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
post :mark_as_spam
end
end
......
......@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do
member do
get 'raw'
get 'download'
post :mark_as_spam
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
t.datetime "queued_at"
t.string "token"
t.integer "lock_version"
t.string "coverage_regex"
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
......
......@@ -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 |
| variables | no | Define build variables |
| cache | no | Define list of files that should be cached between subsequent runs |
| coverage | no | Define coverage settings for all jobs |
### image and services
......@@ -278,6 +279,23 @@ cache:
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
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
......@@ -319,6 +337,7 @@ job_name:
| 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 |
| 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
......@@ -993,6 +1012,25 @@ job:
- 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
> 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](
### 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.
......
@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
end
post ":id/snippets" do
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 = CreateSnippetService.new(user_project, current_user, snippet_params).execute
......
......@@ -64,7 +64,7 @@ module API
desc: 'The visibility level of the snippet'
end
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
if snippet.persisted?
......
......@@ -61,6 +61,7 @@ module Ci
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
......
......@@ -10,13 +10,16 @@ module Gitlab
def find_for_git_client(login, password, project:, ip:)
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 =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) ||
lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
......@@ -143,7 +146,9 @@ module Gitlab
read_authentication_abilities
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
def build_access_token_check(login, password)
......
......@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end
end
end
......@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueCreate,
Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy,
].freeze
......@@ -13,51 +13,32 @@ module Gitlab
if command
if command.allowed?(project, current_user)
present command.new(project, current_user, params).execute(match)
command.new(project, current_user, params).execute(match)
else
access_denied
Gitlab::ChatCommands::Presenters::Access.new.access_denied
end
else
help(help_messages)
Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end
end
def match_command
match = nil
service = available_commands.find do |klass|
match = klass.match(command)
end
service =
available_commands.find do |klass|
match = klass.match(params[:text])
end
[service, match]
end
private
def help_messages
available_commands.map(&:help_message)
end
def available_commands
COMMANDS.select do |klass|
klass.available?(project)
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
module Gitlab
module ChatCommands
class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
......@@ -24,35 +22,29 @@ module Gitlab
to = match[:to]
actions = find_actions(from, to)
return unless actions.present?
if actions.one?
play!(from, to, actions.first)
if actions.none?
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
Result.new(:error, 'Too many actions defined')
Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end
end
private
def play!(from, to, action)
new_action = action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
return unless environment
return [] unless environment
environment.actions_for(to).select(&:starts_environment?)
end
def url(subject)
polymorphic_url(
[subject.project.namespace.becomes(Namespace), subject.project, subject]
)
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 ChatCommands
class IssueCreate < IssueCommand
class IssueNew < IssueCommand
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
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
......@@ -19,8 +19,24 @@ module Gitlab
title = match[:title]
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
end
def presenter(issue)
Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
end
end
end
end
......@@ -10,7 +10,13 @@ module Gitlab
end
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
......
......@@ -10,7 +10,13 @@ module Gitlab
end
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
......
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
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.'
entry :coverage, Entry::Coverage,
description: 'Coverage configuration for this pipeline.'
helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs
:variables, :stages, :types, :cache, :coverage, :jobs
def compose!(_deps = nil)
super(self) do
......
......@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
after_script variables environment]
after_script variables environment coverage]
validations do
validates :config, allowed_keys: ALLOWED_KEYS
......@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
entry :coverage, Entry::Coverage,
description: 'Coverage configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts, :commands, :environment
:artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies
......@@ -130,6 +133,7 @@ module Gitlab
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
after_script: after_script_value }
end
......
......@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
def validate_regexp(value)
!value.nil? && Regexp.new(value.to_s) && true
rescue RegexpError, TypeError
false
end
def validate_string_or_regexp(value)
return true if value.is_a?(Symbol)
return false unless value.is_a?(String)
if value.first == '/' && value.last == '/'
Regexp.new(value[1...-1])
validate_regexp(value[1...-1])
else
true
end
rescue RegexpError
false
end
def validate_boolean(value)
......
......@@ -9,15 +9,7 @@ module Gitlab
include Validatable
validations do
include LegacyValidationHelpers
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
validates :config, array_of_strings_or_regexps: true
end
end
end
......
......@@ -54,6 +54,51 @@ module Gitlab
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
def validate_each(record, attribute, value)
type = options[:with]
......
......@@ -41,6 +41,8 @@ module Gitlab
end
def ensure_default_member!
@project.project_members.destroy_all
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
......
......@@ -6,8 +6,8 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) }
before do
project.team << [user, :master]
project.team << [user2, :master]
project.add_master(user)
project.add_master(user2)
end
describe 'GET #index' do
......@@ -69,6 +69,86 @@ describe Projects::SnippetsController do
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|
describe "GET ##{action}" do
context 'when the project snippet is private' do
......
......@@ -138,6 +138,65 @@ describe SnippetsController do
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|
describe "GET #{action}" 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
visit_environment(environment)
end
scenario 'shows environment name' do
expect(page).to have_content(environment.name)
end
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
......
......@@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do
end
scenario 'does create a new pipeline' do
expect(page).to have_content('Production')
expect(page).to have_content('production')
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