Commit ed3b1698 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 05f4b2fb
......@@ -30,7 +30,7 @@ export default {
EnvironmentsBlock,
ErasedBlock,
Icon,
Log: () => (isNewJobLogActive() ? import('./job_log_json.vue') : import('./job_log.vue')),
Log: () => (isNewJobLogActive() ? import('./log/log.vue') : import('./job_log.vue')),
LogTopBar,
StuckBlock,
UnmetPrerequisitesBlock,
......
<script>
export default {
name: 'JobLogJSON',
};
</script>
<template>
<pre>
{{ __('This feature is in development. Please disable the `job_log_json` feature flag') }}
</pre>
</template>
......@@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
include SessionlessAuthentication
include ConfirmEmailWarning
include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern
before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
......
......@@ -9,9 +9,16 @@ module Projects
end
def execute
Projects::HousekeepingService.new(@project).execute do
service = Projects::HousekeepingService.new(@project)
service.execute do
repository.delete_all_refs_except(RESERVED_REF_PREFIXES)
end
# Right now we don't actually have a way to know if a project
# import actually changed, so we increment the counter to avoid
# causing GC to run every time.
service.increment!
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info( # rubocop:disable Gitlab/RailsLogger
"Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}")
......
......@@ -4,7 +4,7 @@
%fieldset
- if omnibus_protected_paths_throttle?
.bs-callout.bs-callout-danger
- relative_url_link = 'https://docs.gitlab.com/ee/user/admin_area/settings/protected_paths.html#migrating-from-omnibus'
- relative_url_link = 'https://docs.gitlab.com/ee/user/admin_area/settings/protected_paths.html#migrate-settings-from-gitlab-123-and-earlier'
- relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link }
= _("Omnibus Protected Paths throttle is active. From 12.4, Omnibus throttle is deprecated and will be removed in a future release. Please read the %{relative_url_link_start}Migrating Protected Paths documentation%{relative_url_link_end}.").html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
......
---
title: Fixes wrong link on Protected paths admin settings
merge_request: 17945
author:
type: other
......@@ -88,6 +88,7 @@ Example response:
"due_date_from_milestones": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
"labels": [],
"upvotes": 4,
"downvotes": 0
......@@ -143,6 +144,7 @@ Example response:
"due_date_from_milestones": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
"labels": [],
"upvotes": 4,
"downvotes": 0
......@@ -209,6 +211,7 @@ Example response:
"due_date_from_milestones": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
"labels": [],
"upvotes": 4,
"downvotes": 0
......@@ -276,6 +279,7 @@ Example response:
"due_date_from_milestones": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
"labels": [],
"upvotes": 4,
"downvotes": 0
......@@ -358,7 +362,8 @@ Example response:
"start_date": null,
"end_date": null,
"created_at": "2018-01-21T06:21:13.165Z",
"updated_at": "2018-01-22T12:41:41.166Z"
"updated_at": "2018-01-22T12:41:41.166Z",
"closed_at": "2018-08-18T12:22:05.239Z"
},
"target_url": "https://gitlab.example.com/groups/epics/5",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
......
......@@ -80,7 +80,7 @@ With the purpose of being [respectful of others' time](https://about.gitlab.com/
1. Before writing code, ensure your vision of the architecture is aligned with
GitLab's architecture.
1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it.
1. Add a diagram to the issue and ask a frontend maintainer in the slack channel `#frontend_maintainers` about it.
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
......
# GraphQL
Our GraphQL API can be explored via GraphiQL at your instance's
`/-/graphql-explorer` or at [GitLab.com](https://gitlab.com/-/graphql-explorer).
You can check all existing queries and mutations on the right side
of GraphiQL in its **Documentation explorer**. It's also possible to
write queries and mutations directly on the left tab and check
their execution by clicking **Execute query** button on the top left:
![GraphiQL interface](img/graphiql_explorer_v12_4.png)
We use [Apollo] and [Vue Apollo][vue-apollo] for working with GraphQL
on the frontend.
In order to use GraphQL, you need to enable the `graphql` feature flag,
read more about [Feature Flags][feature-flags].
## Apollo Client
To save duplicated clients getting created in different apps, we have a
......
# frozen_string_literal: true
# == Experimentation
#
# Utility module used for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
# The feature_toggle and environment keys are optional. If the feature_toggle is not set, a feature with the name of
# the experiment will be checked, with a default value of true. The enabled_ratio is required and should be
# the ratio for the number of users for which this experiment is enabled. For example: a ratio of 0.1 will
# enable the experiment for 10% of the users (determined by the `experimentation_subject_index`).
#
module Gitlab
module Experimentation
EXPERIMENTS = {
signup_flow: {
feature_toggle: :experimental_separate_sign_up_flow,
environment: ::Gitlab.dev_env_or_com?,
enabled_ratio: 0.1
}
}.freeze
# Controller concern that checks if an experimentation_subject_id cookie is present and sets it if absent.
# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
# to controllers and views.
#
module ControllerConcern
extend ActiveSupport::Concern
included do
before_action :set_experimentation_subject_id_cookie
helper_method :experiment_enabled?
end
def set_experimentation_subject_id_cookie
return if cookies[:experimentation_subject_id].present?
cookies.permanent.signed[:experimentation_subject_id] = {
value: SecureRandom.uuid,
domain: :all,
secure: ::Gitlab.config.gitlab.https
}
end
def experiment_enabled?(experiment)
Experimentation.enabled?(experiment, experimentation_subject_index)
end
private
def experimentation_subject_index
experimentation_subject_id = cookies.signed[:experimentation_subject_id]
return if experimentation_subject_id.blank?
experimentation_subject_id.delete('-').hex % 100
end
end
class << self
def enabled?(experiment_key, experimentation_subject_index)
return false unless EXPERIMENTS.key?(experiment_key)
experiment = Experiment.new(EXPERIMENTS[experiment_key].merge(key: experiment_key))
experiment.feature_toggle_enabled? &&
experiment.enabled_for_environment? &&
experiment.enabled_for_experimentation_subject?(experimentation_subject_index)
end
end
Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, keyword_init: true) do
def feature_toggle_enabled?
return Feature.enabled?(key, default_enabled: true) if feature_toggle.nil?
Feature.enabled?(feature_toggle)
end
def enabled_for_environment?
return true if environment.nil?
environment
end
def enabled_for_experimentation_subject?(experimentation_subject_index)
return false if enabled_ratio.nil? || experimentation_subject_index.blank?
experimentation_subject_index <= enabled_ratio * 100
end
end
end
end
......@@ -16074,9 +16074,6 @@ msgstr ""
msgid "This environment has no deployments yet."
msgstr ""
msgid "This feature is in development. Please disable the `job_log_json` feature flag"
msgstr ""
msgid "This feature requires local storage to be enabled"
msgstr ""
......
......@@ -189,7 +189,7 @@ describe Projects::DiscussionsController do
context "when vue_mr_discussions cookie is present" do
before do
allow(controller).to receive(:cookies).and_return({ vue_mr_discussions: 'true' })
cookies[:vue_mr_discussions] = 'true'
end
it "renders discussion with serializer" do
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { __ } from '~/locale';
import List from '~/ide/components/branches/search_list.vue';
import Item from '~/ide/components/branches/item.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { branches } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('IDE branches search list', () => {
let wrapper;
const fetchBranchesMock = jest.fn();
const createComponent = (state, currentBranchId = 'branch') => {
const fakeStore = new Vuex.Store({
state: {
currentBranchId,
currentProjectId: 'project',
},
modules: {
branches: {
namespaced: true,
state: { isLoading: false, branches: [], ...state },
actions: {
fetchBranches: fetchBranchesMock,
},
},
},
});
wrapper = shallowMount(List, {
localVue,
store: fakeStore,
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('calls fetch on mounted', () => {
createComponent();
expect(fetchBranchesMock).toHaveBeenCalled();
});
it('renders loading icon when `isLoading` is true', () => {
createComponent({ isLoading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders branches not found when search is not empty and branches list is empty', () => {
createComponent({ branches: [] });
wrapper.find('input[type="search"]').setValue('something');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.text()).toContain(__('No branches found'));
});
});
describe('with branches', () => {
it('renders list', () => {
createComponent({ branches });
const items = wrapper.findAll(Item);
expect(items.length).toBe(branches.length);
});
it('renders check next to active branch', () => {
const activeBranch = 'regular';
createComponent({ branches }, activeBranch);
const items = wrapper.findAll(Item).filter(w => w.props('isActive'));
expect(items.length).toBe(1);
expect(items.at(0).props('item').name).toBe(activeBranch);
});
});
});
import { TEST_HOST } from 'spec/test_constants';
export const projectData = {
id: 1,
name: 'abcproject',
web_url: '',
avatar_url: '',
path: '',
name_with_namespace: 'namespace/abcproject',
branches: {
master: {
treeId: 'abcproject/master',
can_push: true,
commit: {
id: '123',
},
},
},
mergeRequests: {},
merge_requests_enabled: true,
default_branch: 'master',
};
export const pipelines = [
{
id: 1,
ref: 'master',
sha: '123',
details: {
status: {
icon: 'status_failed',
group: 'failed',
text: 'Failed',
},
},
commit: { id: '123' },
},
{
id: 2,
ref: 'master',
sha: '213',
details: {
status: {
icon: 'status_failed',
group: 'failed',
text: 'Failed',
},
},
commit: { id: '213' },
},
];
export const stages = [
{
dropdown_path: `${TEST_HOST}/testing`,
name: 'build',
status: {
icon: 'status_failed',
group: 'failed',
text: 'failed',
},
},
{
dropdown_path: 'testing',
name: 'test',
status: {
icon: 'status_failed',
group: 'failed',
text: 'failed',
},
},
];
export const jobs = [
{
id: 1,
name: 'test',
path: 'testing',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 2,
name: 'test 2',
path: 'testing2',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 3,
name: 'test 3',
path: 'testing3',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 4,
name: 'test 4',
path: 'testing4',
status: {
icon: 'status_failed',
text: 'failed',
},
stage: 'build',
duration: 1,
started: new Date(),
},
];
export const fullPipelinesResponse = {
data: {
count: {
all: 2,
},
pipelines: [
{
id: '51',
path: 'test',
commit: {
id: '123',
},
details: {
status: {
icon: 'status_failed',
text: 'failed',
},
stages: [...stages],
},
},
{
id: '50',
commit: {
id: 'abc123def456ghi789jkl',
},
details: {
status: {
icon: 'status_success',
text: 'passed',
},
stages: [...stages],
},
},
],
},
};
export const mergeRequests = [
{
id: 1,
iid: 1,
title: 'Test merge request',
project_id: 1,
web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`,
},
];
export const branches = [
{
id: 1,
name: 'master',
commit: {
message: 'Update master branch',
committed_date: '2018-08-01T00:20:05Z',
},
can_push: true,
protected: true,
default: true,
},
{
id: 2,
name: 'protected/no-access',
commit: {
message: 'Update some stuff',
committed_date: '2018-08-02T00:00:05Z',
},
can_push: false,
protected: true,
default: false,
},
{
id: 3,
name: 'protected/access',
commit: {
message: 'Update some stuff',
committed_date: '2018-08-02T00:00:05Z',
},
can_push: true,
protected: true,
default: false,
},
{
id: 4,
name: 'regular',
commit: {
message: 'Update some more stuff',
committed_date: '2018-06-30T00:20:05Z',
},
can_push: true,
protected: false,
default: false,
},
{
id: 5,
name: 'regular/no-access',
commit: {
message: 'Update some more stuff',
committed_date: '2018-06-30T00:20:05Z',
},
can_push: false,
protected: false,
default: false,
},
];
import Vue from 'vue';
import store from '~/ide/stores';
import * as types from '~/ide/stores/modules/branches/mutation_types';
import List from '~/ide/components/branches/search_list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { branches as testBranches } from '../../mock_data';
import { resetStore } from '../../helpers';
describe('IDE branches search list', () => {
const Component = Vue.extend(List);
let vm;
beforeEach(() => {
vm = createComponentWithStore(Component, store, {});
spyOn(vm, 'fetchBranches');
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
it('calls fetch on mounted', () => {
expect(vm.fetchBranches).toHaveBeenCalledWith({
search: '',
});
});
it('renders loading icon', done => {
vm.$store.state.branches.isLoading = true;
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainElement('.loading-container');
})
.then(done)
.catch(done.fail);
});
it('renders branches not found when search is not empty', done => {
vm.search = 'testing';
vm.$nextTick(() => {
expect(vm.$el).toContainText('No branches found');
done();
});
});
describe('with branches', () => {
const currentBranch = testBranches[1];
beforeEach(done => {
vm.$store.state.currentBranchId = currentBranch.name;
vm.$store.commit(`branches/${types.RECEIVE_BRANCHES_SUCCESS}`, testBranches);
vm.$nextTick(done);
});
it('renders list', () => {
const elementText = Array.from(vm.$el.querySelectorAll('li strong')).map(x =>
x.textContent.trim(),
);
expect(elementText).toEqual(testBranches.map(x => x.name));
});
it('renders check next to active branch', () => {
const checkedText = Array.from(vm.$el.querySelectorAll('li'))
.filter(x => x.querySelector('.ide-search-list-current-icon svg'))
.map(x => x.querySelector('strong').textContent.trim());
expect(checkedText).toEqual([currentBranch.name]);
});
});
});
import { TEST_HOST } from '../test_constants';
export const projectData = {
id: 1,
name: 'abcproject',
web_url: '',
avatar_url: '',
path: '',
name_with_namespace: 'namespace/abcproject',
branches: {
master: {
treeId: 'abcproject/master',
can_push: true,
commit: {
id: '123',
},
},
},
mergeRequests: {},
merge_requests_enabled: true,
default_branch: 'master',
};
export const pipelines = [
{
id: 1,
ref: 'master',
sha: '123',
details: {
status: {
icon: 'status_failed',
group: 'failed',
text: 'Failed',
},
},
commit: { id: '123' },
},
{
id: 2,
ref: 'master',
sha: '213',
details: {
status: {
icon: 'status_failed',
group: 'failed',
text: 'Failed',
},
},
commit: { id: '213' },
},
];
export const stages = [
{
dropdown_path: `${TEST_HOST}/testing`,
name: 'build',
status: {
icon: 'status_failed',
group: 'failed',
text: 'failed',
},
},
{
dropdown_path: 'testing',
name: 'test',
status: {
icon: 'status_failed',
group: 'failed',
text: 'failed',
},
},
];
export const jobs = [
{
id: 1,
name: 'test',
path: 'testing',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 2,
name: 'test 2',
path: 'testing2',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 3,
name: 'test 3',
path: 'testing3',
status: {
icon: 'status_success',
text: 'passed',
},
stage: 'test',
duration: 1,
started: new Date(),
},
{
id: 4,
name: 'test 4',
path: 'testing4',
status: {
icon: 'status_failed',
text: 'failed',
},
stage: 'build',
duration: 1,
started: new Date(),
},
];
export const fullPipelinesResponse = {
data: {
count: {
all: 2,
},
pipelines: [
{
id: '51',
path: 'test',
commit: {
id: '123',
},
details: {
status: {
icon: 'status_failed',
text: 'failed',
},
stages: [...stages],
},
},
{
id: '50',
commit: {
id: 'abc123def456ghi789jkl',
},
details: {
status: {
icon: 'status_success',
text: 'passed',
},
stages: [...stages],
},
},
],
},
};
export const mergeRequests = [
{
id: 1,
iid: 1,
title: 'Test merge request',
project_id: 1,
web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`,
},
];
export const branches = [
{
id: 1,
name: 'master',
commit: {
message: 'Update master branch',
committed_date: '2018-08-01T00:20:05Z',
},
can_push: true,
protected: true,
default: true,
},
{
id: 2,
name: 'protected/no-access',
commit: {
message: 'Update some stuff',
committed_date: '2018-08-02T00:00:05Z',
},
can_push: false,
protected: true,
default: false,
},
{
id: 3,
name: 'protected/access',
commit: {
message: 'Update some stuff',
committed_date: '2018-08-02T00:00:05Z',
},
can_push: true,
protected: true,
default: false,
},
{
id: 4,
name: 'regular',
commit: {
message: 'Update some more stuff',
committed_date: '2018-06-30T00:20:05Z',
},
can_push: true,
protected: false,
default: false,
},
{
id: 5,
name: 'regular/no-access',
commit: {
message: 'Update some more stuff',
committed_date: '2018-06-30T00:20:05Z',
},
can_push: false,
protected: false,
default: false,
},
];
export * from '../../frontend/ide/mock_data';
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Experimentation::ControllerConcern, type: :controller do
controller(ApplicationController) do
include Gitlab::Experimentation::ControllerConcern
def index
head :ok
end
end
describe '#set_experimentation_subject_id_cookie' do
before do
get :index
end
context 'cookie is present' do
before do
cookies[:experimentation_subject_id] = 'test'
end
it 'does not change the cookie' do
expect(cookies[:experimentation_subject_id]).to eq 'test'
end
end
context 'cookie is not present' do
it 'sets a permanent signed cookie' do
expect(cookies.permanent.signed[:experimentation_subject_id]).to be_present
end
end
end
describe '#experiment_enabled?' do
context 'cookie is not present' do
it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of nil' do
expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, nil)
controller.experiment_enabled?(:test_experiment)
end
end
context 'cookie is present' do
before do
cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
get :index
end
it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
# 'abcd1234'.hex % 100 = 76
expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, 76)
controller.experiment_enabled?(:test_experiment)
end
end
end
end
describe Gitlab::Experimentation do
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
test_experiment: {
feature_toggle: feature_toggle,
environment: environment,
enabled_ratio: enabled_ratio
}
})
stub_feature_flags(feature_toggle => true)
end
let(:feature_toggle) { :test_experiment_toggle }
let(:environment) { Rails.env.test? }
let(:enabled_ratio) { 0.1 }
describe '.enabled?' do
subject { described_class.enabled?(:test_experiment, experimentation_subject_index) }
let(:experimentation_subject_index) { 9 }
context 'feature toggle is enabled, we are on the right environment and we are selected' do
it { is_expected.to be_truthy }
end
describe 'experiment is not defined' do
it 'returns false' do
expect(described_class.enabled?(:missing_experiment, experimentation_subject_index)).to be_falsey
end
end
describe 'feature toggle' do
context 'feature toggle is not set' do
let(:feature_toggle) { nil }
it { is_expected.to be_truthy }
end
context 'feature toggle is not set, but a feature with the experiment key as name does exist' do
before do
stub_feature_flags(test_experiment: false)
end
let(:feature_toggle) { nil }
it { is_expected.to be_falsey }
end
context 'feature toggle is disabled' do
before do
stub_feature_flags(feature_toggle => false)
end
it { is_expected.to be_falsey }
end
end
describe 'environment' do
context 'environment is not set' do
let(:environment) { nil }
it { is_expected.to be_truthy }
end
context 'we are on the wrong environment' do
let(:environment) { ::Gitlab.com? }
it { is_expected.to be_falsey }
end
end
describe 'enabled ratio' do
context 'enabled ratio is not set' do
let(:enabled_ratio) { nil }
it { is_expected.to be_falsey }
end
context 'experimentation_subject_index is not set' do
let(:experimentation_subject_index) { nil }
it { is_expected.to be_falsey }
end
context 'experimentation_subject_index is an empty string' do
let(:experimentation_subject_index) { '' }
it { is_expected.to be_falsey }
end
context 'experimentation_subject_index outside enabled ratio' do
let(:experimentation_subject_index) { 11 }
it { is_expected.to be_falsey }
end
end
end
end
......@@ -19,6 +19,8 @@ describe Projects::AfterImportService do
allow(housekeeping_service)
.to receive(:execute).and_yield
expect(housekeeping_service).to receive(:increment!)
end
it 'performs housekeeping' do
......
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