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
  ...
This commit is contained in:
Filipa Lacerda 2018-03-01 11:13:12 +00:00
commit edbf6204c0
64 changed files with 429 additions and 139 deletions

View File

@ -45,3 +45,4 @@ exclude_paths:
- log/
- backups/
- coverage-javascript/
- plugins/

1
.gitignore vendored
View File

@ -66,3 +66,4 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
/plugins/*

View File

@ -1,4 +1,4 @@
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
retry: 1

View File

@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
- 'plugins/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a

View File

@ -4,7 +4,9 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super('boards');
super({
page: 'boards',
});
this.store = store;
this.updateUrl = updateUrl;

View File

@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#environments-list-view',
components: {
environmentsComponent,
@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});

View File

@ -1,10 +0,0 @@
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';

View File

@ -10,13 +10,22 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
constructor({
baseEndpoint = '',
tokenizer,
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor;
this.setupMapping();
@ -59,7 +68,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/milestones.json`,
endpoint: this.getMilestoneEndpoint(),
symbol: '%',
},
element: this.container.querySelector('#js-dropdown-milestone'),
@ -68,7 +77,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json`,
endpoint: this.getLabelsEndpoint(),
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
@ -90,6 +99,18 @@ export default class FilteredSearchDropdownManager {
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) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');

View File

@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager {
constructor({
page,
isGroup = false,
isGroupAncestor = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
this.isGroup = false;
this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@ -75,13 +78,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager(
this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer,
this.page,
this.isGroup,
this.filteredSearchTokenKeys,
);
this.dropdownManager = new FilteredSearchDropdownManager({
baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,

View File

@ -0,0 +1,3 @@
import initEnviroments from '~/environments/';
document.addEventListener('DOMContentLoaded', initEnviroments);

View File

@ -0,0 +1,14 @@
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();
}

View File

@ -1,15 +1,7 @@
import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initShow();
initSidebarBundle();
});

View File

@ -0,0 +1,33 @@
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();
}

View File

@ -1,33 +1,7 @@
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 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';
import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initShow();
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();
});

View File

@ -1,3 +1,7 @@
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines);
document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});

View File

@ -1,3 +1,7 @@
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines);
document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});

View File

@ -1,7 +1,13 @@
/* 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 initDeployKeys from '~/deploy_keys';
document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
});

View File

@ -1,9 +1,21 @@
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');
if (filteredSearchEnabled) {
const filteredSearchManager = new FilteredSearchManager({ page });
const filteredSearchManager = new FilteredSearchManager({
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
stateFiltersSelector,
});
filteredSearchManager.setup();
}
};

View File

@ -9,7 +9,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
});
};

View File

@ -1,9 +0,0 @@
/* 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();
});

View File

@ -39,7 +39,7 @@
@click="onClick">
...
</button>
<span v-show="!isCollapsed">
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>

View File

@ -14,6 +14,11 @@
collapsedCalendarIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
@ -91,7 +96,10 @@
</script>
<template>
<div class="block">
<div
class="block"
:class="blockClass"
>
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"

View File

@ -11,6 +11,8 @@ class SystemHooksService
SystemHook.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks')
end
Gitlab::Plugin.execute_all_async(data)
end
private

View File

@ -1,8 +1,5 @@
- page_title "Merge Requests"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else

View File

@ -28,4 +28,5 @@
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= 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

View File

@ -6,6 +6,3 @@
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} }
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')

View File

@ -6,8 +6,6 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"

View File

@ -70,6 +70,7 @@
Enable or disable certain project features and choose access levels.
.settings-content
= 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)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"

View File

@ -1,9 +1,6 @@
- @no_container = true
- 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),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,

View File

@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag("common_vue")
= webpack_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,

View File

@ -1,7 +1,5 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
.prometheus-container{ class: container_class }
.top-area

View File

@ -74,6 +74,7 @@
= _("Commits per day hour (UTC)")
%canvas#hour-chart
-# haml-lint:disable InlineJavaScript
%script#projectChartData{ type: "application/json" }
- projectChartData = {};
- projectChartData['hour'] = @commits_per_time

View File

@ -55,6 +55,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
-# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
%h2.title= markdown_field(@issue, :title)

View File

@ -4,4 +4,5 @@
%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

View File

@ -26,6 +26,7 @@
= _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|

View File

@ -20,4 +20,5 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= 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

View File

@ -13,4 +13,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('pipelines_details')

View File

@ -1,6 +1,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('protected_tags')
- content_for :create_protected_tag do
= render 'projects/protected_tags/create_protected_tag'

View File

@ -43,4 +43,5 @@
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= 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

View File

@ -7,6 +7,7 @@
- content_for :page_specific_javascripts do
= 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-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal

View File

@ -1,6 +1,4 @@
- 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' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
@ -119,10 +117,12 @@
= render partial: "shared/issuable/label_page_create"
- 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
#js-confidential-entry-point
- 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
#js-lock-entry-point
@ -159,4 +159,5 @@
= _('Move')
= 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

View File

@ -1,7 +1,5 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- 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' }
.issuable-sidebar.milestone-sidebar

View File

@ -35,4 +35,5 @@
is locked. Only
%b project members
can comment.
-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe

View File

@ -1,6 +1,7 @@
#js-authenticate-u2f
%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" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).

View File

@ -1,5 +1,6 @@
#js-register-u2f
-# haml-lint:disable InlineJavaScript
%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).

View File

@ -84,6 +84,7 @@
- new_note
- pages
- pages_domain_verification
- plugin
- post_receive
- process_commit
- project_cache

View File

@ -0,0 +1,15 @@
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

View File

@ -0,0 +1,5 @@
---
title: Add ability to use external plugins as an alternative to system hooks
merge_request: 17003
author:
type: added

View File

@ -68,3 +68,4 @@
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
- [plugin, 1]

View File

@ -1,33 +1,33 @@
'use strict';
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var glob = require('glob');
var webpack = require('webpack');
var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
var NameAllModulesPlugin = require('name-all-modules-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const NameAllModulesPlugin = require('name-all-modules-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION;
const ROOT_PATH = path.resolve(__dirname, '..');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
var autoEntriesCount = 0;
var watchAutoEntries = [];
let autoEntriesCount = 0;
let watchAutoEntries = [];
function generateEntries() {
// generate automatic entry points
var autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
const autoEntries = {};
const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
watchAutoEntries = [
path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
];
@ -44,22 +44,15 @@ function generateEntries() {
const manualEntries = {
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',
mr_notes: './mr_notes/index.js',
notebook_viewer: './blob/notebook_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_tags: './protected_tags',
registry_list: './registry/index.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
ui_development_kit: './ui_development_kit.js',
two_factor_auth: './two_factor_auth.js',
common: './commons/index.js',
@ -76,7 +69,7 @@ function generateEntries() {
return Object.assign(manualEntries, autoEntries);
}
var config = {
const config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
@ -168,7 +161,7 @@ var config = {
new StatsWriterPlugin({
filename: 'manifest.json',
transform: function(data, opts) {
var stats = opts.compiler.getStats().toJson({
const stats = opts.compiler.getStats().toJson({
chunkModules: false,
source: false,
chunks: false,

View File

@ -0,0 +1,66 @@
# 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

26
lib/gitlab/plugin.rb Normal file
View File

@ -0,0 +1,26 @@
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

View File

@ -0,0 +1,7 @@
module Gitlab
class PluginLogger < Gitlab::Logger
def self.file_name_noext
'plugin'
end
end
end

View File

@ -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)')
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

16
lib/tasks/plugins.rake Normal file
View File

@ -0,0 +1,16 @@
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

View File

@ -0,0 +1,3 @@
#!/usr/bin/env clojure
(let [in (slurp *in*)]
(spit "/tmp/clj-data.txt" in))

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
x = STDIN.read
File.write('/tmp/rb-data.txt', x)

View File

@ -126,7 +126,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./diff_notes/components/resolve_count.js',
'./dispatcher.js',
'./environments/environments_bundle.js',
'./filtered_search/filtered_search_bundle.js',
'./graphs/graphs_bundle.js',
'./issuable/time_tracking/time_tracking_bundle.js',
'./main.js',

View File

@ -0,0 +1,68 @@
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

View File

@ -1,7 +1,7 @@
require 'spec_helper'
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(:stages) { table(:ci_stages) }
let(:pipelines) { table(:ci_pipelines) }

View File

@ -1,7 +1,7 @@
require 'spec_helper'
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(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }

View File

@ -700,7 +700,7 @@ describe API::Runner do
context 'when tace is given' 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)
end

View File

@ -1,4 +1,4 @@
# pack-refs with: peeled fully-peeled
# pack-refs with: peeled fully-peeled sorted
0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature
12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix
6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path

View File

@ -0,0 +1,25 @@
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