Commit 23c928c0 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ee-40629-icon-styles

parents fa266b14 07203ff6
......@@ -604,7 +604,7 @@ codequality:
script:
- cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml
artifacts:
......
......@@ -114,7 +114,7 @@ gem 'google-api-client', '~> 0.13.6'
gem 'unf', '~> 0.1.4'
# Seed data
gem 'seed-fu', '~> 2.3.5'
gem 'seed-fu', '~> 2.3.7'
# Search
gem 'elasticsearch-model', '~> 0.1.9'
......@@ -295,7 +295,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta37'
gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
gem 'raindrops', '~> 0.18'
end
......
......@@ -654,7 +654,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta37)
prometheus-client-mmap (0.7.0.beta39)
mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -844,7 +844,7 @@ GEM
rake (>= 0.9, < 13)
sass (~> 3.4.20)
securecompare (1.0.0)
seed-fu (2.3.6)
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
select2-rails (3.5.9.3)
......@@ -1149,7 +1149,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta37)
prometheus-client-mmap (~> 0.7.0.beta39)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
......@@ -583,6 +583,13 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
.then(ciCdSettings => ciCdSettings.default())
.catch((err) => {
Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
throw err;
});
case 'groups:settings:ci_cd:show':
new ProjectVariables();
break;
......
......@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
......@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
IGNORE_HIDING_CLASS,
};
import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown {
constructor(list) {
constructor(list, config = {}) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
this.eventWrapper = {};
if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
}
this.getItems();
this.initTemplateString();
this.addEvents();
......@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected);
e.preventDefault();
this.hide();
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', {
detail: {
......@@ -67,7 +70,20 @@ class DropDown {
addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
this.list.addEventListener('click', this.eventWrapper.clickEvent);
this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
}
closeDropdown(event) {
// `ESC` key closes the dropdown.
if (event.keyCode === 27) {
event.preventDefault();
return this.toggle();
}
return true;
}
setData(data) {
......@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
}
hide() {
......@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
}
toggle() {
......@@ -128,6 +148,7 @@ class DropDown {
destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
}
static setImagesSrc(template) {
......
......@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook {
constructor(trigger, list, plugins, config) {
this.trigger = trigger;
this.list = new DropDown(list);
this.list = new DropDown(list, config);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
......
......@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
`;
const removeFlashClickListener = (flashEl, fadeTransition) => {
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
/*
......
......@@ -16,6 +16,10 @@ export default {
required: true,
type: String,
},
updateEndpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
......@@ -262,6 +266,8 @@ export default {
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
/>
<edited-component
v-if="hasUpdated"
......
......@@ -22,6 +22,16 @@
required: false,
default: '',
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
},
data() {
return {
......@@ -48,7 +58,7 @@
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
dataType: 'issue',
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
});
......@@ -95,7 +105,9 @@
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
v-model="descriptionText">
v-model="descriptionText"
:data-update-url="updateUrl"
>
</textarea>
</div>
</template>
......@@ -79,7 +79,7 @@
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
class="btn-blank btn-edit note-action-button"
class="btn btn-default btn-edit btn-svg"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
......
......@@ -56,9 +56,11 @@ export const slugify = str => str.trim().toLowerCase();
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
/**
* Capitalizes first character.
* Capitalizes first character
*
* @param {String} text
* @returns {String}
* @return {String}
*/
export const capitalizeFirstCharacter = text => `${text[0].toUpperCase()}${text.slice(1)}`;
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
......@@ -319,6 +319,8 @@ $(function () {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
removeFlashClickListener(flashContainer.children[0]);
flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
removeFlashClickListener(flashEl);
});
}
});
function updateAutoDevopsRadios(radioWrappers) {
radioWrappers.forEach((radioWrapper) => {
const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
if (runPipelineCheckbox) {
runPipelineCheckbox.checked = radio.checked;
runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
}
});
}
export default function initCiCdSettings() {
const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
radioWrappers.forEach(radioWrapper =>
radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
);
}
......@@ -12,6 +12,9 @@
/>
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
props: {
name: {
......@@ -22,7 +25,10 @@
size: {
type: Number,
required: false,
default: 0,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
cssClasses: {
......@@ -42,10 +48,11 @@
},
};
</script>
<template>
<svg
:class="[iconSizeClass, cssClasses]">
<use
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
</template>
......@@ -125,7 +125,7 @@
@include transition(border-color);
}
.note-action-button .link-highlight,
.note-action-button,
.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
......
......@@ -88,17 +88,6 @@
border-color: $border-dark;
color: $color;
}
svg {
path {
fill: $color;
}
use {
stroke: $color;
}
}
}
@mixin btn-green {
......@@ -142,6 +131,13 @@
}
}
@mixin btn-svg {
height: $gl-padding;
width: $gl-padding;
top: 0;
vertical-align: text-top;
}
.btn {
@include btn-default;
@include btn-white;
......@@ -444,3 +440,7 @@
text-decoration: none;
}
}
.btn-svg svg {
@include btn-svg;
}
......@@ -2,14 +2,43 @@
.cgray { color: $common-gray; }
.clgray { color: $common-gray-light; }
.cred { color: $common-red; }
svg.cred { fill: $common-red; }
.cgreen { color: $common-green; }
svg.cgreen { fill: $common-green; }
.cdark { color: $common-gray-dark; }
.text-plain,
.text-plain:hover {
color: $gl-text-color;
}
.text-secondary {
color: $gl-text-color-secondary;
}
.text-primary,
.text-primary:hover {
color: $brand-primary;
}
.text-success,
.text-success:hover {
color: $brand-success;
}
.text-danger,
.text-danger:hover {
color: $brand-danger;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
}
.text-info,
.text-info:hover {
color: $brand-info;
}
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; }
......
......@@ -34,8 +34,15 @@
}
}
.flash-success {
@extend .alert;
@extend .alert-success;
margin: 0;
}
.flash-notice,
.flash-alert {
.flash-alert,
.flash-success {
border-radius: $border-radius-default;
.container-fluid,
......@@ -48,7 +55,8 @@
margin-bottom: 0;
.flash-notice,
.flash-alert {
.flash-alert,
.flash-success {
border-radius: 0;
}
}
......
......@@ -195,33 +195,6 @@ summary {
}
}
// Typography =================================================================
.text-primary,
.text-primary:hover {
color: $brand-primary;
}
.text-success,
.text-success:hover {
color: $brand-success;
}
.text-danger,
.text-danger:hover {
color: $brand-danger;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
}
.text-info,
.text-info:hover {
color: $brand-info;
}
// Prevent datetimes on tooltips to break into two lines
.local-timeago {
white-space: nowrap;
......
......@@ -70,14 +70,13 @@
.title {
padding: 0;
margin-bottom: 16px;
margin-bottom: $gl-padding;
border-bottom: 0;
}
.btn-edit {
margin-left: auto;
// Set height to match title height
height: 2em;
height: $gl-padding * 2;
}
// Border around images in issue and MR descriptions.
......@@ -292,6 +291,8 @@
.gutter-toggle {
margin-top: 7px;
border-left: 1px solid $border-gray-normal;
padding-left: 0;
text-align: center;
}
.title .gutter-toggle {
......
......@@ -218,7 +218,24 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
@include new-style-dropdown;
.branch-message,
.ref-message {
display: none;
}
.ref::selection {
color: $placeholder-text-color;
}
.dropdown {
.dropdown-menu-toggle {
min-width: 285px;
}
.dropdown-select {
width: 285px;
}
}
.btn-group:not(.hide) {
display: flex;
......@@ -229,15 +246,16 @@ ul.related-merge-requests > li {
flex-shrink: 0;
}
.dropdown-menu {
.create-merge-request-dropdown-menu {
width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
display: none;
margin-top: 4px;
}
.dropdown-toggle {
.create-merge-request-dropdown-toggle {
.fa-caret-down {
pointer-events: none;
color: inherit;
......@@ -245,18 +263,50 @@ ul.related-merge-requests > li {
}
}
.droplab-item-ignore {
pointer-events: auto;
}
.create-item {
cursor: pointer;
margin: 0 1px;
&:hover,
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
}
li.divider {
margin: 8px 10px;
}
li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
}
.icon-container {
float: left;
padding-left: 6px;
i {
visibility: hidden;
......@@ -264,13 +314,12 @@ ul.related-merge-requests > li {
}
.description {
padding-left: 30px;
font-size: 13px;
padding-left: 22px;
}
strong {
display: block;
font-weight: $gl-font-weight-bold;
}
input,
span {
margin: 4px 0 0;
}
}
}
......
......@@ -543,10 +543,7 @@ ul.notes {
}
svg {
height: 16px;
width: 16px;
top: 0;
vertical-align: text-top;
@include btn-svg;
}
.award-control-icon-positive,
......@@ -780,12 +777,6 @@ ul.notes {
}
}
svg {
fill: currentColor;
height: 16px;
width: 16px;
}
.loading {
margin: 0;
height: auto;
......
......@@ -893,10 +893,6 @@ pre.light-well {
font-size: $gl-font-size;
}
a {
color: $gl-text-color;
}
.avatar-container,
.controls {
flex: 0 0 auto;
......
......@@ -54,7 +54,7 @@ module IssuableActions
end
def destroy
issuable.destroy
Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
......
......@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController
private
def set_commits
render_404 unless request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
......
......@@ -161,7 +161,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
......@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
if @project.update(update_params)
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to project_settings_ci_cd_path(@project)
else
render 'show'
Projects::UpdateService.new(project, current_user, update_params).tap do |service|
if service.execute
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
if service.run_auto_devops_pipeline?
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
end
redirect_to project_settings_ci_cd_path(@project)
else
render 'show'
end
end
end
......@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
......
class RunnerJobsFinder
attr_reader :runner, :params
def initialize(runner, params = {})
@runner = runner
@params = params
end
def execute
items = @runner.builds
items = by_status(items)
items
end
private
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
end
......@@ -178,6 +178,9 @@ module ApplicationSettingsHelper
:ed25519_key_restriction,
:email_author_in_body,
:enabled_git_access_protocol,
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
:gravatar_enabled,
:hashed_storage_enabled,
:help_page_hide_commercial_content,
......
......@@ -8,6 +8,22 @@ module AutoDevopsHelper
!project.ci_service
end
def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
return false if project.repository.gitlab_ci_yml
if project&.auto_devops&.enabled.present?
!project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
else
current_application_settings.auto_devops_enabled?
end
end
def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
return false if project.repository.gitlab_ci_yml
!project.auto_devops_enabled?
end
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.kubernetes_service&.active?
......
......@@ -213,6 +213,7 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
endpoint: issuable_path(issuable),
updateEndpoint: "#{issuable_path(issuable)}.json",
canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
canAdmin: can?(current_user, :"admin_#{issuable.to_ability_name}", issuable),
......
......@@ -185,6 +185,27 @@ class ApplicationSetting < ActiveRecord::Base
end
end
validates :gitaly_timeout_default,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_medium,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_medium,
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default
validates :gitaly_timeout_medium,
numericality: { greater_than_or_equal_to: :gitaly_timeout_fast },
if: :gitaly_timeout_fast
validates :gitaly_timeout_fast,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_fast,
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
......@@ -325,7 +346,10 @@ class ApplicationSetting < ActiveRecord::Base
slack_app_enabled: false,
slack_app_id: nil,
slack_app_secret: nil,
slack_app_verification_token: nil
slack_app_verification_token: nil,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55
}
end
......
......@@ -108,6 +108,7 @@ module Ci
end
before_transition any => [:failed] do |build|
next unless build.project
next if build.retries_max.zero?
if build.retries_count < build.retries_max
......
......@@ -17,6 +17,7 @@ class CommitStatus < ActiveRecord::Base
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
alias_attribute :pipeline_id, :commit_id
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
......@@ -103,26 +104,29 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
next unless commit_status.project
next if transition.loopback?
commit_status.run_after_commit do
if pipeline
if pipeline_id
if complete? || manual?
PipelineProcessWorker.perform_async(pipeline.id)
PipelineProcessWorker.perform_async(pipeline_id)
else
PipelineUpdateWorker.perform_async(pipeline.id)
PipelineUpdateWorker.perform_async(pipeline_id)
end
end
StageUpdateWorker.perform_async(commit_status.stage_id)
ExpireJobCacheWorker.perform_async(commit_status.id)
StageUpdateWorker.perform_async(stage_id)
ExpireJobCacheWorker.perform_async(id)
end
end
after_transition any => :failed do |commit_status|
next unless commit_status.project
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
.new(pipeline.project, nil).execute(self)
.new(project, nil).execute(self)
end
end
end
......
......@@ -8,19 +8,17 @@ class GeoNode < ActiveRecord::Base
has_many :namespaces, through: :geo_node_namespace_links
has_one :status, class_name: 'GeoNodeStatus'
default_values schema: lambda { Gitlab.config.gitlab.protocol },
host: lambda { Gitlab.config.gitlab.host },
port: lambda { Gitlab.config.gitlab.port },
relative_url_root: lambda { Gitlab.config.gitlab.relative_url_root },
default_values url: ->(record) { record.class.current_node_url },
primary: false,
clone_protocol: 'http'
accepts_nested_attributes_for :geo_node_key
validates :host, host: true, presence: true, uniqueness: { case_sensitive: false, scope: :port }
validates :url, presence: true, uniqueness: { case_sensitive: false }
validate :check_url_is_valid
validates :primary, uniqueness: { message: 'node already exists' }, if: :primary
validates :schema, inclusion: %w(http https)
validates :relative_url_root, length: { minimum: 0, allow_nil: false }
validates :access_key, presence: true
validates :encrypted_secret_access_key, presence: true
validates :clone_protocol, presence: true, inclusion: %w(ssh http)
......@@ -35,16 +33,33 @@ class GeoNode < ActiveRecord::Base
before_validation :ensure_access_keys!
scope :with_url_prefix, ->(prefix) { where('url LIKE ?', "#{prefix}%") }
attr_encrypted :secret_access_key,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
mode: :per_attribute_iv,
encode: true
class << self
def current_node_url
RequestStore.fetch('geo_node:current_node_url') do
cfg = Gitlab.config.gitlab
uri = URI.parse("#{cfg.protocol}://#{cfg.host}:#{cfg.port}#{cfg.relative_url_root}")
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
end
def current_node
GeoNode.find_by(url: current_node_url)
end
end
def current?
host == Gitlab.config.gitlab.host &&
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
self.class.current_node_url == url
end
def secondary?
......@@ -55,24 +70,23 @@ class GeoNode < ActiveRecord::Base
secondary? && clone_protocol == 'ssh'
end
def uri
if relative_url_root
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}"
end
def url
value = read_attribute(:url)
value += '/' if value.present? && !value.end_with?('/')
URI.parse(URI::Generic.build(scheme: schema, host: host, port: port, path: relative_url).normalize.to_s)
value
end
def url
uri.to_s
def url=(value)
value += '/' if value.present? && !value.end_with?('/')
write_attribute(:url, value)
@uri = nil
end
def url=(new_url)
new_uri = URI.parse(new_url)
self.schema = new_uri.scheme
self.host = new_uri.host
self.port = new_uri.port
self.relative_url_root = new_uri.path != '/' ? new_uri.path : ''
def uri
@uri ||= URI.parse(url) if url.present?
end
def geo_transfers_url(file_type, file_id)
......@@ -203,7 +217,7 @@ class GeoNode < ActiveRecord::Base
private
def geo_api_url(suffix)
URI.join(uri, "#{uri.path}/", "api/#{API::API.version}/geo/#{suffix}").to_s
URI.join(uri, "#{uri.path}", "api/#{API::API.version}/geo/#{suffix}").to_s
end
def ensure_access_keys!
......@@ -216,11 +230,7 @@ class GeoNode < ActiveRecord::Base
end
def url_helper_args
if relative_url_root
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}"
end
{ protocol: schema, host: host, port: port, script_name: relative_url }
{ protocol: uri.scheme, host: uri.host, port: uri.port, script_name: uri.path }
end
def build_dependents
......@@ -246,13 +256,19 @@ class GeoNode < ActiveRecord::Base
# Prevent locking yourself out
def check_not_adding_primary_as_secondary
if host == Gitlab.config.gitlab.host &&
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
if url == self.class.current_node_url
errors.add(:base, 'Current node must be the primary node or you will be locking yourself out')
end
end
def check_url_is_valid
if uri.present? && !%w[http https].include?(uri.scheme)
errors.add(:url, 'scheme must be http or https')
end
rescue URI::InvalidURIError
errors.add(:url, 'is invalid')
end
def update_clone_url
self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix
end
......
......@@ -64,7 +64,6 @@ class Issue < ActiveRecord::Base
scope :public_only, -> { where(confidential: false) }
after_save :expire_etag_cache
after_commit :update_project_counter_caches, on: :destroy
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
......
......@@ -56,7 +56,6 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
after_commit :update_project_counter_caches, on: :destroy
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
......
module Issuable
class DestroyService < IssuableBaseService
def execute(issuable)
if issuable.destroy
issuable.update_project_counter_caches
end
end
end
end
......@@ -3,6 +3,8 @@ module MergeRequests
prepend EE::MergeRequests::BuildService
def execute
@issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = []
merge_request.source_project = find_source_project
......@@ -109,20 +111,30 @@ module MergeRequests
#
def assign_title_and_description
assign_title_and_description_from_single_commit
assign_title_from_issue
merge_request.title ||= source_branch.titleize.humanize
merge_request.title = wip_title if compare_commits.empty?
append_closes_description
end
def append_closes_description
return unless issue_iid
closes_issue = "Closes ##{issue_iid}"
if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
end
def assign_title_and_description_from_single_commit
commits = compare_commits
return unless commits && commits.count == 1
return unless commits&.count == 1
commit = commits.first
merge_request.title ||= commit.title
......@@ -132,36 +144,19 @@ module MergeRequests
def assign_title_from_issue
return unless issue
case issue
when Issue
merge_request.title ||= "Resolve \"#{issue.title}\""
when ExternalIssue
merge_request.title ||= "Resolve #{issue.title}"
end
end
def append_closes_description
return unless issue_iid
closes_issue = "Closes ##{issue_iid}"
if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
merge_request.title =
case issue
when Issue then "Resolve \"#{issue.title}\""
when ExternalIssue then "Resolve #{issue.title}"
end
end
def issue_iid
return @issue_iid if defined?(@issue_iid)
@issue_iid = source_branch[/\A(\d+)-/, 1]
@issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
end
def issue
return @issue if defined?(@issue)
@issue = target_project.get_issue(issue_iid, current_user)
@issue ||= target_project.get_issue(issue_iid, current_user)
end
end
end
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
def initialize(project, user, params)
# branch - the name of new branch
# ref - the source of new branch.
@branch_name = params[:branch_name]
@issue_iid = params[:issue_iid]
@ref = params[:ref]
super(project, user)
end
def execute
return error('Invalid issue iid') unless issue_iid.present? && issue.present?
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
params[:label_ids] = issue.label_ids if issue.label_ids.any?
......@@ -21,20 +32,16 @@ module MergeRequests
private
def issue_iid
@isssue_iid ||= params.delete(:issue_iid)
end
def issue
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
end
def branch_name
@branch_name ||= issue.to_branch_name
@branch ||= @branch_name || issue.to_branch_name
end
def ref
project.default_branch || 'master'
@ref || project.default_branch || 'master'
end
def merge_request
......@@ -43,6 +50,7 @@ module MergeRequests
def merge_request_params
{
issue_iid: @issue_iid,
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
......
......@@ -25,7 +25,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
if project.update_attributes(params.except(:default_branch))
if project.update_attributes(update_params)
if project.previous_changes.include?('path')
project.rename_repo
else
......@@ -41,8 +41,16 @@ module Projects
end
end
def run_auto_devops_pipeline?
params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
end
private
def update_params
params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
end
def changing_storage_size?
new_repository_storage = params[:repository_storage]
......
......@@ -771,6 +771,30 @@
.help-block
Number of Git pushes after which 'git gc' is run.
%fieldset
%legend Gitaly Timeouts
.form-group
= f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_default, class: 'form-control'
.help-block
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
.form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_fast, class: 'form-control'
.help-block
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
.form-group
= f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_medium, class: 'form-control'
.help-block
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
%fieldset
%legend Web terminal
.form-group
......
.flash-container.flash-container-page
- if alert
.flash-alert
-# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
%div{ class: "flash-#{key}" }
%div{ class: (container_class) }
%span= alert
- elsif notice
.flash-notice
%div{ class: (container_class) }
%span= notice
%span= value
......@@ -67,8 +67,8 @@
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
.status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }
= link_to project_pipeline_path(@project, last_pipeline.id) do
.status-icon-container
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
= link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
......
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- if can?(current_user, :push_code, @project)
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
.btn-group.available.hide
%input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
%button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
%button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
= value
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
%ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
.menu-item
.icon-container
= icon('check')
.description
%strong Create a merge request
%span
Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
%li.divider.droplab-item-ignore
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
.menu-item
.icon-container
= icon('check')
.description
%strong Create a branch
%span
Creates a branch named after this issue, from '#{@project.default_branch}'.
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create merge request and branch
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create branch
%li.divider
%li.droplab-item-ignore
Branch name
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%span.js-branch-message.branch-message.droplab-item-ignore
%li.droplab-item-ignore
Source (branch or tag)
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%span.js-ref-message.ref-message.droplab-item-ignore
%li.droplab-item-ignore
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
Create merge request
......@@ -13,29 +13,39 @@
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
.radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
= form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
- if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project)
.checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
= label_tag 'project[run_auto_devops_pipeline_explicit]' do
= check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
= s_('ProjectSettings|Immediately run a pipeline on the default branch')
.radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
= form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_nil do
= form.radio_button :enabled, ''
.radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_ do
= form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%br
- if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project)
.checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
= label_tag 'project[run_auto_devops_pipeline_implicit]' do
= check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
= s_('ProjectSettings|Immediately run a pipeline on the default branch')
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
......
......@@ -20,7 +20,7 @@
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
%h3.prepend-top-0.append-bottom-0
= link_to project_path(project), class: dom_class(project) do
= link_to project_path(project), class: 'text-plain' do
%span.project-full-name
%span.namespace-name
- if project.namespace && !skip_namespace
......
class CreatePipelineWorker
include Sidekiq::Worker
include PipelineQueue
enqueue_in group: :creation
def perform(project_id, user_id, ref, source, params = {})
project = Project.find(project_id)
user = User.find(user_id)
params = params.deep_symbolize_keys
Ci::CreatePipelineService
.new(project, user, ref: ref)
.execute(source, **params)
end
end
......@@ -45,9 +45,17 @@ class StuckCiJobsWorker
end
def search(status, timeout)
builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
yield(build)
loop do
jobs = Ci::Build.where(status: status)
.where('ci_builds.updated_at < ?', timeout.ago)
.includes(:tags, :runner, project: :namespace)
.limit(100)
.to_a
break if jobs.empty?
jobs.each do |job|
yield(job)
end
end
end
......
---
title: Fix viewing default push rules on a Geo secondary
merge_request: 3559
author:
type: fixed
---
title: Fix Advanced Search Syntax documentation
merge_request: 3571
author:
type: fixed
---
title: Add border for epic edit button
merge_request:
author:
type: other
---
title: Fix tasklist for epics
merge_request:
author:
type: fixed
---
title: Add an ability to use a custom branch name on creation from issues
merge_request: 13884
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: Add the option to automatically run a pipeline after updating AutoDevOps settings
merge_request: 15380
author:
type: changed
---
title: Create issuable destroy service
merge_request: 15604
author: George Andrinopoulos
type: other
---
title: Upgrade seed-fu to 2.3.7
merge_request: 15607
author: Takuya Noguchi
type: other
---
title: Add timeouts for Gitaly calls
merge_request: 15047
author:
type: performance
---
title: Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside
merge_request:
author:
type: performance
---
title: New API endpoint - list jobs for a specified runner
merge_request: 15432
author:
type: added
......@@ -28,6 +28,7 @@
- [build, 2]
- [pipeline, 2]
- [pipeline_processing, 5]
- [pipeline_creation, 4]
- [pipeline_default, 3]
- [pipeline_cache, 3]
- [pipeline_hooks, 2]
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddGitalyTimeoutPropertiesToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings,
:gitaly_timeout_default,
:integer,
default: 55
add_column_with_default :application_settings,
:gitaly_timeout_medium,
:integer,
default: 30
add_column_with_default :application_settings,
:gitaly_timeout_fast,
:integer,
default: 10
end
def down
remove_column :application_settings, :gitaly_timeout_default
remove_column :application_settings, :gitaly_timeout_medium
remove_column :application_settings, :gitaly_timeout_fast
end
end
class StoreGeoNodesUrlDirectly < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
class GeoNode < ActiveRecord::Base
def compute_unified_url!
uri = URI.parse("#{schema}://#{host}#{relative_url_root}")
uri.port = port if port.present?
uri.path += '/' unless uri.path.end_with?('/')
update!(url: uri.to_s)
end
def compute_split_url!
uri = URI.parse(url)
update!(
schema: uri.scheme,
host: uri.host,
port: uri.port,
relative_url_root: uri.path
)
end
end
def up
add_column :geo_nodes, :url, :string
GeoNode.find_each { |node| node.compute_unified_url! }
change_column_null(:geo_nodes, :url, false)
end
def down
GeoNode.find_each { |node| node.compute_split_url! }
remove_column :geo_nodes, :url, :string
end
end
class IndexGeoNodesUrl < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :geo_nodes, :url, unique: true
end
def down
remove_concurrent_index :geo_nodes, :url, unique: true
end
end
class RemoveGeoNodesUrlPartColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
remove_column :geo_nodes, :schema, :string
remove_column :geo_nodes, :host, :string
remove_column :geo_nodes, :port, :integer
remove_column :geo_nodes, :relative_url_root, :string
end
def down
add_column :geo_nodes, :schema, :string
add_column :geo_nodes, :host, :string
add_column :geo_nodes, :port, :integer
add_column :geo_nodes, :relative_url_root, :string
add_concurrent_index :geo_nodes, :host
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171124070437) do
ActiveRecord::Schema.define(version: 20171124165823) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -174,6 +174,9 @@ ActiveRecord::Schema.define(version: 20171124070437) do
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
t.boolean "password_authentication_enabled_for_web"
t.boolean "password_authentication_enabled_for_git", default: true
t.integer "gitaly_timeout_default", default: 55, null: false
t.integer "gitaly_timeout_medium", default: 30, null: false
t.integer "gitaly_timeout_fast", default: 10, null: false
end
create_table "approvals", force: :cascade do |t|
......@@ -956,10 +959,6 @@ ActiveRecord::Schema.define(version: 20171124070437) do
add_index "geo_node_statuses", ["geo_node_id"], name: "index_geo_node_statuses_on_geo_node_id", unique: true, using: :btree
create_table "geo_nodes", force: :cascade do |t|
t.string "schema"
t.string "host"
t.integer "port"
t.string "relative_url_root"
t.boolean "primary"
t.integer "geo_node_key_id"
t.integer "oauth_application_id"
......@@ -971,11 +970,12 @@ ActiveRecord::Schema.define(version: 20171124070437) do
t.integer "files_max_capacity", default: 10, null: false
t.integer "repos_max_capacity", default: 25, null: false
t.string "clone_protocol", default: "http", null: false
t.string "url", null: false
end
add_index "geo_nodes", ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
add_index "geo_nodes", ["host"], name: "index_geo_nodes_on_host", using: :btree
add_index "geo_nodes", ["primary"], name: "index_geo_nodes_on_primary", using: :btree
add_index "geo_nodes", ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree
create_table "geo_repositories_changed_events", id: :bigserial, force: :cascade do |t|
t.integer "geo_node_id", null: false
......
......@@ -215,6 +215,91 @@ DELETE /runners/:id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
```
## List runner's jobs
List jobs that are being processed or were processed by specified Runner.
```
GET /runners/:id/jobs
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a runner |
| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running"
```
Example response:
```json
[
{
"id": 2,
"status": "running",
"stage": "test",
"name": "test",
"ref": "master",
"tag": false,
"coverage": null,
"created_at": "2017-11-16T08:50:29.000Z",
"started_at": "2017-11-16T08:51:29.000Z",
"finished_at": "2017-11-16T08:53:29.000Z",
"duration": 120,
"user": {
"id": 1,
"name": "John Doe2",
"username": "user2",
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
"web_url": "http://localhost/user2",
"created_at": "2017-11-16T18:38:46.000Z",
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": "",
"organization": null
},
"commit": {
"id": "97de212e80737a608d939f648d959671fb0a0142",
"short_id": "97de212e",
"title": "Update configuration\r",
"created_at": "2017-11-16T08:50:28.000Z",
"parent_ids": [
"1b12f15a11fc6e62177bef08f47bc7b5ce50b141",
"498214de67004b1da3d820901307bed2a68a8ef6"
],
"message": "See merge request !123",
"author_name": "John Doe2",
"author_email": "user2@example.org",
"authored_date": "2017-11-16T08:50:27.000Z",
"committer_name": "John Doe2",
"committer_email": "user2@example.org",
"committed_date": "2017-11-16T08:50:27.000Z"
},
"pipeline": {
"id": 2,
"sha": "97de212e80737a608d939f648d959671fb0a0142",
"ref": "master",
"status": "running"
},
"project": {
"id": 1,
"description": null,
"name": "project1",
"name_with_namespace": "John Doe2 / project1",
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2017-11-16T18:38:46.620Z"
}
}
]
```
## List project's runners
List all runners (specific and shared) available in the project. Shared runners
......
......@@ -121,7 +121,7 @@ Google Cloud.
## Enabling Auto DevOps
NOTE: **Note:**
**Note:**
If you haven't done already, read the [prerequisites](#prerequisites) to make
full use of Auto DevOps. If this is your fist time, we recommend you follow the
[quick start guide](#quick-start).
......@@ -129,10 +129,14 @@ full use of Auto DevOps. If this is your fist time, we recommend you follow the
1. Go to your project's **Settings > CI/CD > General pipelines settings** and
find the Auto DevOps section
1. Select "Enable Auto DevOps"
1. After selecting an option to enable Auto DevOps, a checkbox will appear below
so you can immediately run a pipeline on the default branch
1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
that will be used by Kubernetes to deploy your application
1. Hit **Save changes** for the changes to take effect
![Project AutoDevops settings section](img/auto_devops_settings.png)
Now that it's enabled, there are a few more steps depending on whether your project
has a `.gitlab-ci.yml` or not:
......
......@@ -82,7 +82,7 @@ added directly to your configured cluster. Those applications are needed for
| Application | GitLab version | Description |
| ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
## Enabling or disabling the Cluster integration
......
......@@ -7,7 +7,7 @@ work for on-premises installations where you can configure the
with Slack on making this configurable for all GitLab installations, but there's
no ETA.
It was first introduced in GitLab 9.4 and distributed to Slack App Directory in
GitLab 10.2 (with availability toward the end of November 2017).
GitLab 10.2.
Slack provides a native application which you can enable via your project's
integrations on GitLab.com.
......@@ -15,12 +15,9 @@ integrations on GitLab.com.
## Slack App Directory
The simplest way to enable the GitLab Slack application for your workspace is to
install the GitLab application from
install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab) from
the [Slack App Directory](https://slack.com/apps).
> This will be available toward the end of November 2017, and the docs will be updated here
when it is ready.
Clicking install will take you to the
[GitLab Slack application landing page](https://gitlab.com/profile/slack/edit)
where you can select a project to enable the GitLab Slack application for.
......
......@@ -23,13 +23,13 @@ you need the search results to be as efficient as possible. You have a feeling
of what you want to find (e.g., a function name), but at the same you're also
not so sure.
In that case, using the regular expressions in your query will yield much better
results.
In that case, using the advanced search syntax in your query will yield much
better results.
## Using the Advanced Syntax Search
The Advanced Syntax Search supports queries of ranges, wildcards, regular
expressions, fuzziness and much more.
The Advanced Syntax Search supports fuzzy or exact search queries with prefixes,
boolean operators, and much more.
Full details can be found in the [Elasticsearch documentation][elastic], but
here's a quick guide:
......@@ -42,7 +42,6 @@ here's a quick guide:
* To group terms together, use parentheses: `bug | (display +sound)`
* To match a partial word, use `*`: `bug find_by_*`
* To find a term containing one of these symbols, use `\`: `argument \-last`
* To limit the results based on the time "created_at:[2012-01-01 TO 2012-12-31]" and other sweet stuff
[ee]: https://about.gitlab.com/gitlab-ee/
[elastic]: https://www.elastic.co/guide/en/elasticsearch/reference/5.3/query-dsl-simple-query-string-query.html#_simple_query_string_syntax
......@@ -12,6 +12,10 @@
type: String,
required: true,
},
updateEndpoint: {
type: String,
required: true,
},
canUpdate: {
required: true,
type: Boolean,
......@@ -111,7 +115,9 @@
:can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:update-endpoint="updateEndpoint"
:issuable-ref="issuableRef"
issuable-type="epic"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
......
......@@ -110,7 +110,7 @@ class Admin::GeoNodesController < Admin::ApplicationController
end
def has_insecure_nodes?
GeoNode.where(schema: 'http').any?
GeoNode.with_url_prefix('http://').exists?
end
def flash_now(type, message)
......
......@@ -40,6 +40,6 @@ class Admin::PushRulesController < Admin::ApplicationController
end
def push_rule
@push_rule ||= PushRule.find_or_create_by(is_sample: true)
@push_rule ||= PushRule.find_or_initialize_by(is_sample: true)
end
end
......@@ -39,8 +39,14 @@ module EE
def redirect_allowed_to?(uri)
raise NotImplementedError unless defined?(super)
# Redirect is not only allowed to current host, but also to other Geo nodes
super || ::Gitlab::Geo.geo_node?(host: uri.host, port: uri.port)
# Redirect is not only allowed to current host, but also to other Geo
# nodes. relative_url_root *must* be ignored here as we don't know what
# is root and what is path
super || begin
truncated = uri.dup.tap { |uri| uri.path = '/' }
::GeoNode.with_url_prefix(truncated).exists?
end
end
end
end
......@@ -6,7 +6,7 @@
%hr.clearfix
= form_for [:admin, @push_rule] do |f|
= form_for @push_rule, url: admin_push_rule_path, method: :put do |f|
- if @push_rule.errors.any?
.alert.alert-danger
- @push_rule.errors.full_messages.each do |msg|
......
......@@ -90,16 +90,21 @@ module API
expose :group_access, as: :group_access_level
end
class BasicProjectDetails < Grape::Entity
expose :id, :description, :default_branch, :tag_list
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
class ProjectIdentity < Grape::Entity
expose :id, :description
expose :name, :name_with_namespace
expose :path, :path_with_namespace
expose :created_at
end
class BasicProjectDetails < ProjectIdentity
expose :default_branch, :tag_list
expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
expose :star_count, :forks_count
expose :created_at, :last_activity_at
expose :last_activity_at
end
class Project < BasicProjectDetails
......@@ -938,17 +943,24 @@ module API
expose :id, :sha, :ref, :status
end
class Job < Grape::Entity
class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :duration
expose :user, with: User
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: Commit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner
end
class JobBasicWithProject < JobBasic
expose :project, with: ProjectIdentity
end
class Trigger < Grape::Entity
expose :id
expose :token, :description
......
......@@ -261,7 +261,9 @@ module API
authorize!(:destroy_issue, issue)
destroy_conditionally!(issue)
destroy_conditionally!(issue) do |issue|
Issuable::DestroyService.new(user_project, current_user).execute(issue)
end
end
desc 'List merge requests closing issue' do
......
......@@ -179,7 +179,9 @@ module API
authorize!(:destroy_merge_request, merge_request)
destroy_conditionally!(merge_request)
destroy_conditionally!(merge_request) do |merge_request|
Issuable::DestroyService.new(user_project, current_user).execute(merge_request)
end
end
params do
......
......@@ -84,6 +84,23 @@ module API
destroy_conditionally!(runner)
end
desc 'List jobs running on a runner' do
success Entities::JobBasicWithProject
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
use :pagination
end
get ':id/jobs' do
runner = get_runner(params[:id])
authenticate_list_runners_jobs!(runner)
jobs = RunnerJobsFinder.new(runner, params).execute
present paginate(jobs), with: Entities::JobBasicWithProject
end
end
params do
......@@ -192,6 +209,12 @@ module API
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def user_can_access_runner?(runner)
current_user.ci_authorized_runners.exists?(runner.id)
end
......
......@@ -123,6 +123,9 @@ module API
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
......
......@@ -18,11 +18,7 @@ module Gitlab
FDW_SCHEMA = 'gitlab_secondary'.freeze
def self.current_node
self.cache_value(:geo_node_current) do
GeoNode.find_by(host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root)
end
self.cache_value(:geo_node_current) { GeoNode.current_node }
end
def self.primary_node
......@@ -72,10 +68,6 @@ module Gitlab
::License.feature_available?(:geo)
end
def self.geo_node?(host:, port:)
GeoNode.where(host: host, port: port).exists?
end
def self.fdw?
self.cache_value(:geo_fdw?) do
::Geo::BaseRegistry.connection.execute(
......
......@@ -93,11 +93,11 @@ module Gitlab
# kwargs.merge(deadline: Time.now + 10)
# end
#
def self.call(storage, service, rpc, request, remote_storage: nil)
def self.call(storage, service, rpc, request, remote_storage: nil, timeout: nil)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
enforce_gitaly_request_limits(:call)
kwargs = request_kwargs(storage, remote_storage: remote_storage)
kwargs = request_kwargs(storage, timeout, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
......@@ -105,7 +105,7 @@ module Gitlab
self.query_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
end
def self.request_kwargs(storage, remote_storage: nil)
def self.request_kwargs(storage, timeout, remote_storage: nil)
encoded_token = Base64.strict_encode64(token(storage).to_s)
metadata = {
'authorization' => "Bearer #{encoded_token}",
......@@ -117,7 +117,22 @@ module Gitlab
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
{ metadata: metadata }
result = { metadata: metadata }
# nil timeout indicates that we should use the default
timeout = default_timeout if timeout.nil?
return result unless timeout > 0
# Do not use `Time.now` for deadline calculation, since it
# will be affected by Timecop in some tests, but grpc's c-core
# uses system time instead of timecop's time, so tests will fail
# `Time.at(Process.clock_gettime(Process::CLOCK_REALTIME))` will
# circumvent timecop
deadline = Time.at(Process.clock_gettime(Process::CLOCK_REALTIME)) + timeout
result[:deadline] = deadline
result
end
def self.token(storage)
......@@ -290,6 +305,26 @@ module Gitlab
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
end
# The default timeout on all Gitaly calls
def self.default_timeout
return 0 if Sidekiq.server?
timeout(:gitaly_timeout_default)
end
def self.fast_timeout
timeout(:gitaly_timeout_fast)
end
def self.medium_timeout
timeout(:gitaly_timeout_medium)
end
def self.timeout(timeout_name)
Gitlab::CurrentSettings.current_application_settings[timeout_name]
end
private_class_method :timeout
# Count a stack. Used for n+1 detection
def self.count_stack
return unless RequestStore.active?
......
......@@ -16,7 +16,7 @@ module Gitlab
revision: GitalyClient.encode(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request)
response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |msg|
msg.paths.map { |d| EncodingHelper.encode!(d.dup) }
end
......@@ -29,7 +29,7 @@ module Gitlab
child_id: child_id
)
GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request).value
GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request, timeout: GitalyClient.fast_timeout).value
end
def diff(from, to, options = {})
......@@ -77,7 +77,7 @@ module Gitlab
limit: limit.to_i
)
response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request)
response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request, timeout: GitalyClient.medium_timeout)
entry = nil
data = ''
......@@ -102,7 +102,7 @@ module Gitlab
path: path.present? ? GitalyClient.encode(path) : '.'
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |message|
message.entries.map do |gitaly_tree_entry|
......@@ -129,7 +129,7 @@ module Gitlab
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
request.path = options[:path] if options[:path].present?
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request).count
GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end
def last_commit_for_path(revision, path)
......@@ -139,7 +139,7 @@ module Gitlab
path: GitalyClient.encode(path.to_s)
)
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit
return unless gitaly_commit
Gitlab::Git::Commit.new(@repository, gitaly_commit)
......@@ -152,7 +152,7 @@ module Gitlab
to: to
)
response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request)
response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
......@@ -165,7 +165,7 @@ module Gitlab
)
request.order = opts[:order].upcase if opts[:order].present?
response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request)
response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
......@@ -179,7 +179,7 @@ module Gitlab
offset: offset.to_i
)
response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request)
response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
......@@ -197,7 +197,7 @@ module Gitlab
path: GitalyClient.encode(path)
)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
response.reduce("") { |memo, msg| memo << msg.data }
end
......@@ -207,7 +207,7 @@ module Gitlab
revision: GitalyClient.encode(revision)
)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
response.commit
end
......@@ -217,7 +217,7 @@ module Gitlab
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
response.sum(&:data)
end
......@@ -227,7 +227,7 @@ module Gitlab
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
)
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout)
end
def find_commits(options)
......@@ -245,7 +245,7 @@ module Gitlab
request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request)
response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
......@@ -259,7 +259,7 @@ module Gitlab
request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
request = Gitaly::CommitDiffRequest.new(request_params)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout)
GitalyClient::DiffStitcher.new(response)
end
......
......@@ -46,7 +46,8 @@ module Gitlab
commit_id: commit_id,
prefix: ref_prefix
)
encode!(GitalyClient.call(@storage, :ref_service, :find_ref_name, request).name.dup)
response = GitalyClient.call(@storage, :ref_service, :find_ref_name, request, timeout: GitalyClient.medium_timeout)
encode!(response.name.dup)
end
def count_tag_names
......
......@@ -10,7 +10,9 @@ module Gitlab
def exists?
request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :repository_exists, request).exists
response = GitalyClient.call(@storage, :repository_service, :repository_exists, request, timeout: GitalyClient.fast_timeout)
response.exists
end
def garbage_collect(create_bitmap)
......@@ -30,7 +32,8 @@ module Gitlab
def repository_size
request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :repository_size, request).size
response = GitalyClient.call(@storage, :repository_service, :repository_size, request)
response.size
end
def apply_gitattributes(revision)
......@@ -61,7 +64,7 @@ module Gitlab
def has_local_branches?
request = Gitaly::HasLocalBranchesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request)
response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request, timeout: GitalyClient.fast_timeout)
response.value
end
......
......@@ -175,15 +175,7 @@ namespace :geo do
end
def set_primary_geo_node
params = {
schema: Gitlab.config.gitlab.protocol,
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
relative_url_root: Gitlab.config.gitlab.relative_url_root,
primary: true
}
node = GeoNode.new(params)
node = GeoNode.new(primary: true, url: GeoNode.current_node_url)
puts "Saving primary GeoNode with URL #{node.url}".color(:green)
node.save
......
......@@ -249,7 +249,7 @@ describe Admin::GeoNodesController, :postgresql do
end
context 'with a secondary node' do
let(:geo_node) { create(:geo_node, host: 'example.com', port: 80, enabled: true) }
let(:geo_node) { create(:geo_node, url: 'http://example.com') }
context 'when succeed' do
before do
......
......@@ -12,19 +12,22 @@ describe Projects::PipelinesSettingsController do
end
describe 'PATCH update' do
before do
subject do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
project: {
auto_devops_attributes: params
}
project: { auto_devops_attributes: params,
run_auto_devops_pipeline_implicit: 'false',
run_auto_devops_pipeline_explicit: auto_devops_pipeline }
end
context 'when updating the auto_devops settings' do
let(:params) { { enabled: '', domain: 'mepmep.md' } }
let(:auto_devops_pipeline) { 'false' }
it 'redirects to the settings page' do
subject
expect(response).to have_gitlab_http_status(302)
expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.")
end
......@@ -33,11 +36,32 @@ describe Projects::PipelinesSettingsController do
let(:params) { { enabled: '' } }
it 'allows enabled to be set to nil' do
subject
project_auto_devops.reload
expect(project_auto_devops.enabled).to be_nil
end
end
context 'when run_auto_devops_pipeline is true' do
let(:auto_devops_pipeline) { 'true' }
it 'queues a CreatePipelineWorker' do
expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
subject
end
end
context 'when run_auto_devops_pipeline is not true' do
let(:auto_devops_pipeline) { 'false' }
it 'does not queue a CreatePipelineWorker' do
expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args)
subject
end
end
end
end
end
......@@ -3,7 +3,16 @@ require 'spec_helper'
feature 'Update Epic', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:epic) { create(:epic, group: group) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
This is a task list:
- [ ] Incomplete entry 1
MARKDOWN
end
let(:epic) { create(:epic, group: group, description: markdown) }
before do
stub_licensed_features(epics: true)
......@@ -51,6 +60,16 @@ feature 'Update Epic', :js do
expect(page).not_to have_selector('.uploading-container .button-attach-file')
end
it 'updates the tasklist' do
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 1)
expect(page).to have_selector('ul input[checked]', count: 0)
find('.task-list .task-list-item', text: 'Incomplete entry 1').find('input').click
expect(page).to have_selector('ul input[checked]', count: 1)
end
# Autocomplete is disabled for epics until #4084 is resolved
describe 'autocomplete disabled' do
it 'does not open atwho container' do
......
......@@ -2,16 +2,19 @@ require 'rails_helper'
feature 'Geo clone instructions', :js do
include Devise::Test::IntegrationHelpers
include ::EE::GeoHelpers
let(:project) { create(:project, :empty_repo) }
let(:developer) { create(:user) }
background do
primary = create(:geo_node, :primary, schema: 'https', host: 'primary.domain.com', port: 443)
primary.update_attribute(:clone_url_prefix, 'git@primary.domain.com:')
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
primary = create(:geo_node, :primary, url: 'https://primary.domain.com')
primary.update_columns(clone_url_prefix: 'git@primary.domain.com:')
secondary = create(:geo_node)
project.team << [developer, :developer]
stub_current_geo_node(secondary)
project.add_developer(developer)
sign_in(developer)
end
......
......@@ -32,7 +32,6 @@ describe EE::GitlabRoutingHelper do
context 'HTTP' do
before do
allow(helper).to receive(:default_clone_protocol).and_return('http')
primary.update!(schema: 'http')
end
context 'project' do
......@@ -51,7 +50,7 @@ describe EE::GitlabRoutingHelper do
context 'HTTPS' do
before do
allow(helper).to receive(:default_clone_protocol).and_return('https')
primary.update!(schema: 'https')
primary.update!(url: 'https://localhost:123/relative')
end
context 'project' do
......
......@@ -7,9 +7,17 @@ describe RemoveSystemHookFromGeoNodes, :migration do
before do
allow_any_instance_of(WebHookService).to receive(:execute)
node_attrs = {
schema: 'http',
host: 'localhost',
port: 3000
}
create(:system_hook)
geo_nodes.create! attributes_for(:geo_node, :primary)
geo_nodes.create! attributes_for(:geo_node, system_hook_id: create(:system_hook).id)
hook_id = create(:system_hook).id
geo_nodes.create!(node_attrs.merge(primary: true))
geo_nodes.create!(node_attrs.merge(system_hook_id: hook_id, port: 3001))
end
it 'destroy all system hooks for secondary nodes' do
......
FactoryGirl.define do
factory :geo_node do
host { Gitlab.config.gitlab.host }
sequence(:port) {|n| n}
sequence(:url) do |port|
uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}")
uri.port = port
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
trait :ssh do
clone_protocol 'ssh'
......@@ -10,7 +15,13 @@ FactoryGirl.define do
trait :primary do
primary true
port { Gitlab.config.gitlab.port }
url do
uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}")
uri.port = Gitlab.config.gitlab.port
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
end
end
end
require 'rails_helper'
feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
context 'for team members' do
before do
project.team << [user, :developer]
sign_in(user)
end
it 'allows creating a merge request from the issue page' do
visit project_issue_path(project, issue)
perform_enqueued_jobs do
select_dropdown_option('create-mr')
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
wait_for_requests
end
visit project_issue_path(project, issue)
expect(page).to have_content("created branch 1-cherry-coloured-funk")
expect(page).to have_content("mentioned in merge request !1")
end
it 'allows creating a branch from the issue page' do
visit project_issue_path(project, issue)
select_dropdown_option('create-branch')
wait_for_requests
expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
end
context "when there is a referenced merge request" do
let!(:note) do
create(:note, :on_issue, :system, project: project, noteable: issue,
note: "mentioned in #{referenced_mr.to_reference}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
description: "Fixes #{issue.to_reference}", author: user)
end
before do
referenced_mr.cache_merge_request_closes_issues!(user)
visit project_issue_path(project, issue)
end
it 'disables the create branch button' do
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
expect(page).to have_content /1 Related Merge Request/
end
end
context 'when merge requests are disabled' do
before do
project.project_feature.update(merge_requests_access_level: 0)
visit project_issue_path(project, issue)
end
it 'shows only create branch button' do
expect(page).not_to have_button('Create a merge request')
expect(page).to have_button('Create a branch')
end
end
context 'when issue is confidential' do
it 'disables the create branch button' do
issue = create(:issue, :confidential, project: project)
visit project_issue_path(project, issue)
expect(page).not_to have_css('.create-mr-dropdown-wrap')
end
end
end
context 'for visitors' do
before do
visit project_issue_path(project, issue)
end
it 'shows no buttons' do
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
end
end
def select_dropdown_option(option)
find('.create-mr-dropdown-wrap .dropdown-toggle').click
find("li[data-value='#{option}']").click
find('.js-create-merge-request').click
end
end
require 'rails_helper'
describe 'User creates branch and merge request on issue page', :js do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
context 'when signed out' do
before do
visit project_issue_path(project, issue)
end
it "doesn't show 'Create merge request' button" do
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
end
end
context 'when signed in' do
before do
project.add_developer(user)
sign_in(user)
end
context 'when interacting with the dropdown' do
before do
visit project_issue_path(project, issue)
end
# In order to improve tests performance, all UI checks are placed in this test.
it 'shows elements' do
button_create_merge_request = find('.js-create-merge-request')
button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
button_toggle_dropdown.click
dropdown = find('.create-merge-request-dropdown-menu')
page.within(dropdown) do
button_create_target = find('.js-create-target')
input_branch_name = find('.js-branch-name')
input_source = find('.js-ref')
li_create_branch = find("li[data-value='create-branch']")
li_create_merge_request = find("li[data-value='create-mr']")
# Test that all elements are presented.
expect(page).to have_content('Create merge request and branch')
expect(page).to have_content('Create branch')
expect(page).to have_content('Branch name')
expect(page).to have_content('Source (branch or tag)')
expect(page).to have_button('Create merge request')
expect(page).to have_selector('.js-branch-name:focus')
test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
test_branch_name_checking(input_branch_name)
test_source_checking(input_source)
# The button inside dropdown should be disabled if any errors occured.
expect(page).to have_button('Create branch', disabled: true)
end
# The top level button should be disabled if any errors occured.
expect(page).to have_button('Create branch', disabled: true)
end
context 'when branch name is auto-generated' do
it 'creates a merge request' do
perform_enqueued_jobs do
select_dropdown_option('create-mr')
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
wait_for_requests
end
visit project_issue_path(project, issue)
expect(page).to have_content('created branch 1-cherry-coloured-funk')
expect(page).to have_content('mentioned in merge request !1')
end
it 'creates a branch' do
select_dropdown_option('create-branch')
wait_for_requests
expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
end
end
context 'when branch name is custom' do
let(:branch_name) { 'custom-branch-name' }
it 'creates a merge request' do
perform_enqueued_jobs do
select_dropdown_option('create-mr', branch_name)
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
expect(page).to have_content('Request to merge custom-branch-name into')
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
wait_for_requests
end
visit project_issue_path(project, issue)
expect(page).to have_content('created branch custom-branch-name')
expect(page).to have_content('mentioned in merge request !1')
end
it 'creates a branch' do
select_dropdown_option('create-branch', branch_name)
wait_for_requests
expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
expect(current_path).to eq project_tree_path(project, branch_name)
end
end
end
context "when there is a referenced merge request" do
let!(:note) do
create(:note, :on_issue, :system, project: project, noteable: issue,
note: "mentioned in #{referenced_mr.to_reference}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
description: "Fixes #{issue.to_reference}", author: user)
end
before do
referenced_mr.cache_merge_request_closes_issues!(user)
visit project_issue_path(project, issue)
end
it 'disables the create branch button' do
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
expect(page).to have_content /1 Related Merge Request/
end
end
context 'when merge requests are disabled' do
before do
project.project_feature.update(merge_requests_access_level: 0)
visit project_issue_path(project, issue)
end
it 'shows only create branch button' do
expect(page).not_to have_button('Create merge request')
expect(page).to have_button('Create branch')
end
end
context 'when issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it 'disables the create branch button' do
visit project_issue_path(project, issue)
expect(page).not_to have_css('.create-mr-dropdown-wrap')
end
end
end
private
def select_dropdown_option(option, branch_name = nil)
find('.create-mr-dropdown-wrap .dropdown-toggle').click
find("li[data-value='#{option}']").click
if branch_name
find('.js-branch-name').set(branch_name)
# Javascript debounces AJAX calls.
# So we have to wait until AJAX requests are started.
# Details are in app/assets/javascripts/create_merge_request_dropdown.js
# this.refDebounce = _.debounce(...)
sleep 0.5
wait_for_requests
end
find('.js-create-merge-request').click
end
def test_branch_name_checking(input_branch_name)
expect(input_branch_name.value).to eq(issue.to_branch_name)
input_branch_name.set('new-branch-name')
branch_name_message = find('.js-branch-message')
expect(branch_name_message).to have_text('Checking branch name availability…')
wait_for_requests
expect(branch_name_message).to have_text('Branch name is available')
input_branch_name.set(project.default_branch)
expect(branch_name_message).to have_text('Checking branch name availability…')
wait_for_requests
expect(branch_name_message).to have_text('Branch is already taken')
end
def test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
page.within(li_create_merge_request) do
expect(page).to have_css('i.fa.fa-check')
expect(button_create_target).to have_text('Create merge request')
expect(button_create_merge_request).to have_text('Create merge request')
end
li_create_branch.click
page.within(li_create_branch) do
expect(page).to have_css('i.fa.fa-check')
expect(button_create_target).to have_text('Create branch')
expect(button_create_merge_request).to have_text('Create branch')
end
end
def test_source_checking(input_source)
expect(input_source.value).to eq(project.default_branch)
input_source.set('mas') # Intentionally entered first 3 letters of `master` to check autocomplete feature later.
source_message = find('.js-ref-message')
expect(source_message).to have_text('Checking source availability…')
wait_for_requests
expect(source_message).to have_text('Source is not available')
# JavaScript gets refs started with `mas` (entered above) and places the first match.
# User sees `mas` in black color (the part he entered) and the `ter` in gray color (a hint).
# Since hinting is implemented via text selection and rspec/capybara doesn't have matchers for it,
# we just checking the whole source name.
expect(input_source.value).to eq(project.default_branch)
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.
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