Commit edbf6204 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into fl-document-key-vue

* master: (53 commits)
  init sidebar bundle separately to allow EE override
  Add support for custom container class
  Update class init config
  Update FilteredSearchManager init config
  Add group related config and endpoint methods to be consistent with EE
  Tag migration specs with :sidekiq to clear queues between tests
  Add update for packed-refs with git v2.16
  Bump GitLab CI test image to use git v2.16
  Ignore InlineJavaScript linter in existing script tag locations.
  Update inline_javascript.rb to lint uses of the javascript filter as well as script tags.
  Update inline_javascript.rb
  Forbid all inline script tags in Linter::InlineJavaScript
  Remove webpack bundle tag for enviroments
  remove a bunch of superfluous common_vue bundles
  Remove use of any_instance_of in runner spec
  Remove trailing line from plugin logger
  Improve plugins documentation and remove unnecessary rake task
  Refactor plugins feature and make some doc improvements
  prefer let and const in webpack config
  remove common_vue from CommonsChunkPlugin
  ...
parents d7b3a711 7aa9ec7a
...@@ -45,3 +45,4 @@ exclude_paths: ...@@ -45,3 +45,4 @@ exclude_paths:
- log/ - log/
- backups/ - backups/
- coverage-javascript/ - coverage-javascript/
- plugins/
...@@ -66,3 +66,4 @@ eslint-report.html ...@@ -66,3 +66,4 @@ eslint-report.html
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp /locale/**/*.time_stamp
/.rspec /.rspec
/plugins/*
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
......
...@@ -17,6 +17,7 @@ AllCops: ...@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*' - 'bin/**/*'
- 'generator_templates/**/*' - 'generator_templates/**/*'
- 'builds/**/*' - 'builds/**/*'
- 'plugins/**/*'
CacheRootDirectory: tmp CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a # This cop checks whether some constant value isn't a
......
...@@ -4,7 +4,9 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager'; ...@@ -4,7 +4,9 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends FilteredSearchManager { export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
super('boards'); super({
page: 'boards',
});
this.store = store; this.store = store;
this.updateUrl = updateUrl; this.updateUrl = updateUrl;
......
...@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate'; ...@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: '#environments-list-view', el: '#environments-list-view',
components: { components: {
environmentsComponent, environmentsComponent,
...@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
import './dropdown_emoji';
import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
...@@ -10,13 +10,22 @@ import DropdownUser from './dropdown_user'; ...@@ -10,13 +10,22 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager { export default class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) { constructor({
baseEndpoint = '',
tokenizer,
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer; this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys; this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor;
this.setupMapping(); this.setupMapping();
...@@ -59,7 +68,7 @@ export default class FilteredSearchDropdownManager { ...@@ -59,7 +68,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/milestones.json`, endpoint: this.getMilestoneEndpoint(),
symbol: '%', symbol: '%',
}, },
element: this.container.querySelector('#js-dropdown-milestone'), element: this.container.querySelector('#js-dropdown-milestone'),
...@@ -68,7 +77,7 @@ export default class FilteredSearchDropdownManager { ...@@ -68,7 +77,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json`, endpoint: this.getLabelsEndpoint(),
symbol: '~', symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing, preprocessing: DropdownUtils.duplicateLabelPreprocessing,
}, },
...@@ -90,6 +99,18 @@ export default class FilteredSearchDropdownManager { ...@@ -90,6 +99,18 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings; this.mapping = allowedMappings;
} }
getMilestoneEndpoint() {
const endpoint = `${this.baseEndpoint}/milestones.json`;
return endpoint;
}
getLabelsEndpoint() {
const endpoint = `${this.baseEndpoint}/labels.json`;
return endpoint;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
......
...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils'; ...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager { export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false,
isGroupAncestor = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
}) { }) {
this.isGroup = false; this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.states = ['opened', 'closed', 'merged', 'all']; this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page; this.page = page;
...@@ -75,13 +78,14 @@ export default class FilteredSearchManager { ...@@ -75,13 +78,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer; this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager( this.dropdownManager = new FilteredSearchDropdownManager({
this.filteredSearchInput.getAttribute('data-base-endpoint') || '', baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer, tokenizer: this.tokenizer,
this.page, page: this.page,
this.isGroup, isGroup: this.isGroup,
this.filteredSearchTokenKeys, isGroupAncestor: this.isGroupAncestor,
); filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
......
import initEnviroments from '~/environments/';
document.addEventListener('DOMContentLoaded', initEnviroments);
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
export default function () {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
}
import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue'; import initShow from '../show';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new initShow();
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initSidebarBundle(); initSidebarBundle();
}); });
import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../vue_merge_request_widget';
export default function () {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
}
import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable'; import initShow from '../init_merge_request_show';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../../vue_merge_request_widget';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new initShow();
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initSidebarBundle(); initSidebarBundle();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
}); });
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines'; import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines); document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines'; import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines); document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});
/* eslint-disable no-new */
import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys'; import initDeployKeys from '~/deploy_keys';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys(); initDeployKeys();
initSettingsPanels(); initSettingsPanels();
}); });
import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
export default ({ page }) => { export default ({
page,
filteredSearchTokenKeys,
isGroup,
isGroupAncestor,
stateFiltersSelector,
}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) { if (filteredSearchEnabled) {
const filteredSearchManager = new FilteredSearchManager({ page }); const filteredSearchManager = new FilteredSearchManager({
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
stateFiltersSelector,
});
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
}; };
...@@ -9,7 +9,7 @@ import eventHub from './event_hub'; ...@@ -9,7 +9,7 @@ import eventHub from './event_hub';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset; const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
...@@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}, },
}); });
}); };
/* eslint-disable no-unused-vars */
import ProtectedTagCreate from './protected_tag_create';
import ProtectedTagEditList from './protected_tag_edit_list';
$(() => {
const protectedtTagCreate = new ProtectedTagCreate();
const protectedtTagEditList = new ProtectedTagEditList();
});
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
@click="onClick"> @click="onClick">
... ...
</button> </button>
<span v-show="!isCollapsed"> <span v-if="!isCollapsed">
<slot name="expanded"></slot> <slot name="expanded"></slot>
</span> </span>
</span> </span>
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
collapsedCalendarIcon, collapsedCalendarIcon,
}, },
props: { props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: { collapsed: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -91,7 +96,10 @@ ...@@ -91,7 +96,10 @@
</script> </script>
<template> <template>
<div class="block"> <div
class="block"
:class="blockClass"
>
<div class="issuable-sidebar-header"> <div class="issuable-sidebar-header">
<toggle-sidebar <toggle-sidebar
:collapsed="collapsed" :collapsed="collapsed"
......
...@@ -11,6 +11,8 @@ class SystemHooksService ...@@ -11,6 +11,8 @@ class SystemHooksService
SystemHook.hooks_for(hooks_scope).find_each do |hook| SystemHook.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks') hook.async_execute(data, 'system_hooks')
end end
Gitlab::Plugin.execute_all_async(data)
end end
private private
......
- page_title "Merge Requests" - page_title "Merge Requests"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- if group_merge_requests_count(state: 'all').zero? - if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true = render 'shared/empty_states/merge_requests', project_select_button: true
- else - else
......
...@@ -28,4 +28,5 @@ ...@@ -28,4 +28,5 @@
.form-actions .form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
...@@ -6,6 +6,3 @@ ...@@ -6,6 +6,3 @@
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} } } }
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
...@@ -6,8 +6,6 @@ ...@@ -6,8 +6,6 @@
- @content_class = limited_container_width - @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description - page_description @commit.description
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
.container-fluid{ class: [limited_container_width, container_class] } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
......
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
.settings-content .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
-# haml-lint:disable InlineJavaScript
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
......
- @no_container = true - @no_container = true
- page_title "Environments" - page_title "Environments"
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), #environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder, "folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag("common_vue") = webpack_bundle_tag("common_vue")
= webpack_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data, #environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
......
- @no_container = true - @no_container = true
- page_title "Metrics for environment", @environment.name - page_title "Metrics for environment", @environment.name
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
.prometheus-container{ class: container_class } .prometheus-container{ class: container_class }
.top-area .top-area
......
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
= _("Commits per day hour (UTC)") = _("Commits per day hour (UTC)")
%canvas#hour-chart %canvas#hour-chart
-# haml-lint:disable InlineJavaScript
%script#projectChartData{ type: "application/json" } %script#projectChartData{ type: "application/json" }
- projectChartData = {}; - projectChartData = {};
- projectChartData['hour'] = @commits_per_time - projectChartData['hour'] = @commits_per_time
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
-# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app #js-issuable-app
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
......
...@@ -4,4 +4,5 @@ ...@@ -4,4 +4,5 @@
%canvas#build_timesChart{ height: 200 } %canvas#build_timesChart{ height: 200 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe %script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
= _("Pipelines for last year") = _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 } %canvas#yearChart.padded{ height: 250 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" } %script#pipelinesChartsData{ type: "application/json" }
- chartData = [] - chartData = []
- [:week, :month, :year].each do |scope| - [:week, :month, :year].each do |scope|
......
...@@ -20,4 +20,5 @@ ...@@ -20,4 +20,5 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
...@@ -13,4 +13,3 @@ ...@@ -13,4 +13,3 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('pipelines_details')
- content_for :page_specific_javascripts do
= webpack_bundle_tag('protected_tags')
- content_for :create_protected_tag do - content_for :create_protected_tag do
= render 'projects/protected_tags/create_protected_tag' = render 'projects/protected_tags/create_protected_tag'
......
...@@ -43,4 +43,5 @@ ...@@ -43,4 +43,5 @@
.form-actions .form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create' = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
......
- todo = issuable_todo(issuable) - todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
...@@ -119,10 +117,12 @@ ...@@ -119,10 +117,12 @@
= render partial: "shared/issuable/label_page_create" = render partial: "shared/issuable/label_page_create"
- if issuable.has_attribute?(:confidential) - if issuable.has_attribute?(:confidential)
-# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point #js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked) - if issuable.has_attribute?(:discussion_locked)
-# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point #js-lock-entry-point
...@@ -159,4 +159,5 @@ ...@@ -159,4 +159,5 @@
= _('Move') = _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
- affix_offset = local_assigns.fetch(:affix_offset, "50") - affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project] - project = local_assigns[:project]
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar .issuable-sidebar.milestone-sidebar
......
...@@ -35,4 +35,5 @@ ...@@ -35,4 +35,5 @@
is locked. Only is locked. Only
%b project members %b project members
can comment. can comment.
-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe %script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
#js-authenticate-u2f #js-authenticate-u2f
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code %a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
-# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-not-supported{ type: "text/template" } %script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
......
#js-register-u2f #js-register-u2f
-# haml-lint:disable InlineJavaScript
%script#js-register-u2f-not-supported{ type: "text/template" } %script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
......
...@@ -84,6 +84,7 @@ ...@@ -84,6 +84,7 @@
- new_note - new_note
- pages - pages
- pages_domain_verification - pages_domain_verification
- plugin
- post_receive - post_receive
- process_commit - process_commit
- project_cache - project_cache
......
class PluginWorker
include ApplicationWorker
sidekiq_options retry: false
def perform(file_name, data)
success, message = Gitlab::Plugin.execute(file_name, data)
unless success
Gitlab::PluginLogger.error("Plugin Error => #{file_name}: #{message}")
end
true
end
end
---
title: Add ability to use external plugins as an alternative to system hooks
merge_request: 17003
author:
type: added
...@@ -68,3 +68,4 @@ ...@@ -68,3 +68,4 @@
- [project_migrate_hashed_storage, 1] - [project_migrate_hashed_storage, 1]
- [storage_migrator, 1] - [storage_migrator, 1]
- [pages_domain_verification, 1] - [pages_domain_verification, 1]
- [plugin, 1]
'use strict'; 'use strict';
var crypto = require('crypto'); const crypto = require('crypto');
var fs = require('fs'); const fs = require('fs');
var path = require('path'); const path = require('path');
var glob = require('glob'); const glob = require('glob');
var webpack = require('webpack'); const webpack = require('webpack');
var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
var NameAllModulesPlugin = require('name-all-modules-plugin'); const NameAllModulesPlugin = require('name-all-modules-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
var ROOT_PATH = path.resolve(__dirname, '..'); const ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_PRODUCTION = process.env.NODE_ENV === 'production';
var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT; const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION; const NO_COMPRESSION = process.env.NO_COMPRESSION;
var autoEntriesCount = 0; let autoEntriesCount = 0;
var watchAutoEntries = []; let watchAutoEntries = [];
function generateEntries() { function generateEntries() {
// generate automatic entry points // generate automatic entry points
var autoEntries = {}; const autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') }); const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
watchAutoEntries = [ watchAutoEntries = [
path.join(ROOT_PATH, 'app/assets/javascripts/pages/'), path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
]; ];
...@@ -44,22 +44,15 @@ function generateEntries() { ...@@ -44,22 +44,15 @@ function generateEntries() {
const manualEntries = { const manualEntries = {
balsamiq_viewer: './blob/balsamiq_viewer.js', balsamiq_viewer: './blob/balsamiq_viewer.js',
environments: './environments/environments_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
help: './help/help.js',
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
mr_notes: './mr_notes/index.js', mr_notes: './mr_notes/index.js',
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines_details: './pipelines/pipeline_details_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags',
registry_list: './registry/index.js', registry_list: './registry/index.js',
sketch_viewer: './blob/sketch_viewer.js', sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
ui_development_kit: './ui_development_kit.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
common: './commons/index.js', common: './commons/index.js',
...@@ -76,7 +69,7 @@ function generateEntries() { ...@@ -76,7 +69,7 @@ function generateEntries() {
return Object.assign(manualEntries, autoEntries); return Object.assign(manualEntries, autoEntries);
} }
var config = { const config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'), context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries, entry: generateEntries,
...@@ -168,7 +161,7 @@ var config = { ...@@ -168,7 +161,7 @@ var config = {
new StatsWriterPlugin({ new StatsWriterPlugin({
filename: 'manifest.json', filename: 'manifest.json',
transform: function(data, opts) { transform: function(data, opts) {
var stats = opts.compiler.getStats().toJson({ const stats = opts.compiler.getStats().toJson({
chunkModules: false, chunkModules: false,
source: false, source: false,
chunks: false, chunks: false,
......
# Plugins
**Note:** Plugins must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [system hooks] or [webhooks] as an option if you do not
have filesystem access.
Introduced in GitLab 10.6.
A plugin will run on each event so it's up to you to filter events or projects within a plugin code. You can have as many plugins as you want. Each plugin will be triggered by GitLab asynchronously in case of an event. For a list of events please see [system hooks] documentation.
## Setup
Plugins must be placed directly into `plugins` directory, subdirectories will be ignored.
There is an `example` directory inside `plugins` where you can find some basic examples.
Follow the steps below to set up a custom hook:
1. On the GitLab server, navigate to the project's plugin directory.
For an installation from source the path is usually
`/home/git/gitlab/plugins/`. For Omnibus installs the path is
usually `/opt/gitlab/embedded/service/gitlab-rails/plugins`.
1. Inside the `plugins` directory, create a file with a name of your choice, but without spaces or special characters.
1. Make the hook file executable and make sure it's owned by the git user.
1. Write the code to make the plugin function as expected. Plugin can be
in any language. Ensure the 'shebang' at the top properly reflects the language
type. For example, if the script is in Ruby the shebang will probably be
`#!/usr/bin/env ruby`.
1. The data to the plugin will be provided as JSON on STDIN. It will be exactly same as one for [system hooks]
That's it! Assuming the plugin code is properly implemented the hook will fire
as appropriate. Plugins file list is updated for each event. There is no need to restart GitLab to apply a new plugin.
If a plugin executes with non-zero exit code or GitLab fails to execute it, a
message will be logged to `plugin.log`.
## Validation
Writing own plugin can be tricky and its easier if you can check it without altering the system.
We provided a rake task you can use with staging environment to test your plugin before using it in production.
The rake task will use a sample data and execute each of plugins. By output you should be able to determine if
system sees your plugin and if it was executed without errors.
```bash
# Omnibus installations
sudo gitlab-rake plugins:validate
# Installations from source
bundle exec rake plugins:validate RAILS_ENV=production
```
Example of output can be next:
```
-> bundle exec rake plugins:validate RAILS_ENV=production
Validating plugins from /plugins directory
* /home/git/gitlab/plugins/save_to_file.clj succeed (zero exit code)
* /home/git/gitlab/plugins/save_to_file.rb failure (non-zero exit code)
```
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[system hooks]: ../system_hooks/system_hooks.md
[webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
module Gitlab
module Plugin
def self.files
Dir.glob(Rails.root.join('plugins/*')).select do |entry|
File.file?(entry)
end
end
def self.execute_all_async(data)
args = files.map { |file| [file, data] }
PluginWorker.bulk_perform_async(args)
end
def self.execute(file, data)
result = Gitlab::Popen.popen_with_detail([file]) do |stdin|
stdin.write(data.to_json)
end
exit_status = result.status&.exitstatus
[exit_status.zero?, result.stderr]
rescue => e
[false, e.message]
end
end
end
module Gitlab
class PluginLogger < Gitlab::Logger
def self.file_name_noext
'plugin'
end
end
end
...@@ -12,6 +12,12 @@ unless Rails.env.production? ...@@ -12,6 +12,12 @@ unless Rails.env.production?
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)') record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end end
def visit_tag(node)
return unless node.tag_name == 'script'
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
end end
end end
end end
namespace :plugins do
desc 'Validate existing plugins'
task validate: :environment do
puts 'Validating plugins from /plugins directory'
Gitlab::Plugin.files.each do |file|
success, message = Gitlab::Plugin.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA)
if success
puts "* #{file} succeed (zero exit code)."
else
puts "* #{file} failure (non-zero exit code). #{message}"
end
end
end
end
#!/usr/bin/env clojure
(let [in (slurp *in*)]
(spit "/tmp/clj-data.txt" in))
#!/usr/bin/env ruby
x = STDIN.read
File.write('/tmp/rb-data.txt', x)
...@@ -126,7 +126,6 @@ if (process.env.BABEL_ENV === 'coverage') { ...@@ -126,7 +126,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./diff_notes/components/resolve_count.js', './diff_notes/components/resolve_count.js',
'./dispatcher.js', './dispatcher.js',
'./environments/environments_bundle.js', './environments/environments_bundle.js',
'./filtered_search/filtered_search_bundle.js',
'./graphs/graphs_bundle.js', './graphs/graphs_bundle.js',
'./issuable/time_tracking/time_tracking_bundle.js', './issuable/time_tracking/time_tracking_bundle.js',
'./main.js', './main.js',
......
require 'spec_helper'
describe Gitlab::Plugin do
describe '.execute' do
let(:data) { Gitlab::DataBuilder::Push::SAMPLE_DATA }
let(:plugin) { Rails.root.join('plugins', 'test.rb') }
let(:tmp_file) { Tempfile.new('plugin-dump') }
let(:result) { described_class.execute(plugin.to_s, data) }
let(:success) { result.first }
let(:message) { result.last }
let(:plugin_source) do
<<~EOS
#!/usr/bin/env ruby
x = STDIN.read
File.write('#{tmp_file.path}', x)
EOS
end
before do
File.write(plugin, plugin_source)
end
after do
FileUtils.rm(plugin)
end
context 'successful execution' do
before do
File.chmod(0o777, plugin)
end
after do
tmp_file.close!
end
it { expect(success).to be true }
it { expect(message).to be_empty }
it 'ensures plugin received data via stdin' do
result
expect(File.read(tmp_file.path)).to eq(data.to_json)
end
end
context 'non-executable' do
it { expect(success).to be false }
it { expect(message).to include('Permission denied') }
end
context 'non-zero exit' do
let(:plugin_source) do
<<~EOS
#!/usr/bin/env ruby
exit 1
EOS
end
before do
File.chmod(0o777, plugin)
end
it { expect(success).to be false }
it { expect(message).to be_empty }
end
end
end
require 'spec_helper' require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb') require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb')
describe MigrateStagesStatuses, :migration do describe MigrateStagesStatuses, :sidekiq, :migration do
let(:jobs) { table(:ci_builds) } let(:jobs) { table(:ci_builds) }
let(:stages) { table(:ci_stages) } let(:stages) { table(:ci_stages) }
let(:pipelines) { table(:ci_pipelines) } let(:pipelines) { table(:ci_pipelines) }
......
require 'spec_helper' require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration') require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
describe ScheduleBuildStageMigration, :migration do describe ScheduleBuildStageMigration, :sidekiq, :migration do
let(:projects) { table(:projects) } let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) } let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) } let(:stages) { table(:ci_stages) }
......
...@@ -700,7 +700,7 @@ describe API::Runner do ...@@ -700,7 +700,7 @@ describe API::Runner do
context 'when tace is given' do context 'when tace is given' do
it 'creates a trace artifact' do it 'creates a trace artifact' do
allow_any_instance_of(BuildFinishedWorker).to receive(:perform).with(job.id) do allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
CreateTraceArtifactWorker.new.perform(job.id) CreateTraceArtifactWorker.new.perform(job.id)
end end
......
# pack-refs with: peeled fully-peeled # pack-refs with: peeled fully-peeled sorted
0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature 0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature
12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix 12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix
6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path 6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path
......
require 'spec_helper'
describe PluginWorker do
include RepoHelpers
let(:filename) { 'my_plugin.rb' }
let(:data) { { 'event_name' => 'project_create' } }
subject { described_class.new }
describe '#perform' do
it 'executes Gitlab::Plugin with expected values' do
allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([true, ''])
expect(subject.perform(filename, data)).to be_truthy
end
it 'logs message in case of plugin execution failure' do
allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([false, 'permission denied'])
expect(Gitlab::PluginLogger).to receive(:error)
expect(subject.perform(filename, data)).to be_truthy
end
end
end
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