Commit 22ab2db9 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into ee-refactor-protected-branches

parents d804410d efd27a92
......@@ -45,3 +45,4 @@ exclude_paths:
- log/
- backups/
- coverage-javascript/
- plugins/
......@@ -67,3 +67,4 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
/plugins/*
......@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
- 'plugins/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a
......
<script>
import { mapActions } from 'vuex';
import router from '~/ide/ide_router';
import icon from '../../../vue_shared/components/icon.vue';
export default {
......@@ -24,20 +25,27 @@
...mapActions([
'discardFileChanges',
]),
openFileInEditor(file) {
router.push(`/project${file.url}`);
},
},
};
</script>
<template>
<div class="multi-file-commit-list-item">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>
<span class="multi-file-commit-list-path">
{{ file.path }}
</span>
<button
type="button"
class="multi-file-commit-list-path"
@click="openFileInEditor(file)">
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>{{ file.path }}
</span>
</button>
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
......
......@@ -39,7 +39,7 @@
@click="onClick">
...
</button>
<span v-show="!isCollapsed">
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>
......
......@@ -409,25 +409,30 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-list {
flex: 1;
overflow: auto;
padding: $gl-padding;
padding: $gl-padding 0;
min-height: 60px;
}
.multi-file-commit-list-item {
display: flex;
margin-bottom: $grid-size;
padding: 0;
align-items: center;
> svg {
min-width: 16px;
}
.multi-file-discard-btn {
display: none;
margin-left: auto;
color: $gl-link-color;
padding: 0 2px;
&:focus,
&:hover {
text-decoration: underline;
}
}
&:hover {
background: $white-normal;
.multi-file-discard-btn {
display: block;
}
......@@ -460,7 +465,36 @@ table.table tr td.multi-file-table-name {
}
.multi-file-commit-list-path {
padding: $grid-size / 2;
padding-left: $gl-padding;
background: none;
border: 0;
text-align: left;
width: 100%;
min-width: 0;
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
&:hover,
&:focus {
outline: 0;
}
}
.multi-file-commit-list-file-path {
@include str-truncated(100%);
&:hover {
text-decoration: underline;
}
&:active {
text-decoration: none;
}
}
.multi-file-commit-form {
......
......@@ -17,12 +17,20 @@
# sort: string
# non_archived: boolean
# my_reaction_emoji: string
# source_branch: string
# target_branch: string
#
class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
def filter_items(_items)
items = by_source_branch(super)
by_target_branch(items)
end
private
def by_assignee(items)
......@@ -37,6 +45,26 @@ class MergeRequestsFinder < IssuableFinder
items
end
def source_branch
@source_branch ||= params[:source_branch].presence
end
def by_source_branch(items)
return items unless source_branch
items.where(source_branch: source_branch)
end
def target_branch
@target_branch ||= params[:target_branch].presence
end
def by_target_branch(items)
return items unless target_branch
items.where(target_branch: target_branch)
end
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
......
......@@ -13,6 +13,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
......
......@@ -22,6 +22,7 @@
= render 'projects/fork_suggestion'
- if @project.feature_available?(:file_locks)
-# haml-lint:disable InlineJavaScript
%script#js-file-lock{ type: "application/json" }
- data = {}
- data[:path] = @path
......
......@@ -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
......@@ -79,6 +79,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"
......
......@@ -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
......
......@@ -58,6 +58,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)
......
......@@ -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
......@@ -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|
......
......@@ -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
......@@ -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
......@@ -9,6 +9,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
%script#js-board-promotion{ type: "text/x-template" }= render "shared/promotions/promote_issue_board"
......
......@@ -125,10 +125,12 @@
= render 'shared/promotions/promote_issue_weights'
- 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
......@@ -165,4 +167,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
......@@ -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
#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).
......
#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).
......
......@@ -84,6 +84,7 @@
- new_note
- pages
- pages_domain_verification
- plugin
- post_receive
- process_commit
- 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
---
title: Add support for filtering by source and target branch to merge requests API
merge_request:
author:
type: added
......@@ -68,6 +68,7 @@
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
- [plugin, 1]
# EE-specific queues
- [ldap_group_sync, 2]
......
# 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
......@@ -47,6 +47,8 @@ Parameters:
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
......@@ -162,6 +164,8 @@ Parameters:
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
......
<script>
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
export default {
name: 'ModalDast',
components: {
ExpandButton,
Icon,
},
props: {
title: {
type: String,
required: true,
default: '',
},
targetId: {
type: String,
required: false,
default: '',
},
description: {
type: String,
required: true,
default: '',
},
instances: {
type: Array,
required: false,
default: () => ([]),
},
},
computed: {
instancesLabel() {
return s__('ciReport|Instances');
},
},
mounted() {
$(this.$el).on('hidden.bs.modal', () => {
this.$emit('clearData');
});
},
};
</script>
<template>
<div
:id="targetId"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog modal-lg"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
{{ title }}
</h4>
</div>
<div class="modal-body">
{{ description }}
<h5 class="prepend-top-20">{{ instancesLabel }}</h5>
<ul
v-if="instances"
class="report-block-list"
>
<li
v-for="(instance, i) in instances"
:key="i"
class="report-block-list-item-modal failed"
>
<icon
class="report-block-icon"
name="status_failed_borderless"
:size="32"
/>
{{ instance.method }}
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ instance.uri }}
</a>
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block report-block-dast-code prepend-top-10">{{ instance.evidence }}</pre>
</expand-button>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Modal from './dast_modal.vue';
import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
const modalDefaultData = {
modalId: 'modal-mrwidget-issue',
......@@ -16,6 +17,7 @@
components: {
Modal,
Icon,
ExpandButton,
},
props: {
issues: {
......@@ -79,6 +81,11 @@
return this.type === 'dast';
},
},
mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData();
});
},
methods: {
shouldRenderPriority(issue) {
return this.hasPriority && issue.priority;
......@@ -117,85 +124,140 @@
};
</script>
<template>
<ul class="report-block-list">
<li
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral
}"
class="report-block-list-item"
v-for="(issue, index) in issues"
:key="index"
>
<icon
class="report-block-icon"
:name="iconName"
:size="32"
/>
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
<template v-if="isTypeDocker">
<a
v-if="issue.nameLink"
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.name }}
</a>
<template v-else>
{{ issue.name }}
</template>
</template>
<template v-else-if="isTypeDast">
<button
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="btn-link btn-blank btn-open-modal"
:data-target="modalTargetId"
<div>
<ul class="report-block-list">
<li
class="report-block-list-issue"
v-for="(issue, index) in issues"
:key="index"
>
<div
class="report-block-list-icon append-right-5"
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral,
}"
>
{{ issue.name }}
</button>
</template>
<template v-else>
{{ issue.name }}<template v-if="issue.score">:
<strong>{{ formatScore(issue.score) }}</strong></template>
</template>
<icon
:name="iconName"
:size="32"
/>
</div>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text append-right-5">
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
<template v-if="isTypePerformance && issue.delta != null">
({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }})
</template>
<template v-if="isTypeDocker">
<a
v-if="issue.nameLink"
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ issue.name }}</a>
<template v-else>
{{ issue.name }}
</template>
</template>
<template v-else-if="isTypeDast">
<button
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="js-modal-dast btn-link btn-blank text-left break-link"
:data-target="modalTargetId"
>
{{ issue.name }}
</button>
</template>
<template v-else>
{{ issue.name }}<template v-if="issue.score">:
<strong>{{ formatScore(issue.score) }}</strong></template>
</template>
<template v-if="issue.path">
in
<template v-if="isTypePerformance && issue.delta != null">
({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }})
</template>
</div>
<div class="report-block-list-issue-description-link">
<template v-if="issue.path">
in
<a
v-if="issue.urlPath"
:href="issue.urlPath"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a>
<template v-else>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</template>
</template>
</li>
<a
v-if="issue.urlPath"
:href="issue.urlPath"
target="_blank"
rel="noopener noreferrer nofollow"
class="break-link"
>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a>
<template v-else>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</template>
</template>
</div>
</div>
</li>
</ul>
<modal
:target-id="modalId"
:title="modalTitle"
:hide-footer="true"
:description="modalDesc"
:instances="modalInstances"
@clearData="clearModalData()"
/>
</ul>
v-if="isTypeDast"
:id="modalId"
:header-title-text="modalTitle"
ref="modal"
class="modal-security-report-dast"
>
<slot>
{{ modalDesc }}
<h5 class="prepend-top-20">
{{ s__('ciReport|Instances') }}
</h5>
<ul
v-if="modalInstances"
class="report-block-list"
>
<li
v-for="(instance, i) in modalInstances"
:key="i"
class="report-block-list-issue"
>
<div class="report-block-list-icon append-right-5 failed">
<icon
name="status_failed_borderless"
:size="32"
/>
</div>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text append-right-5">
{{ instance.method }}
</div>
<div class="report-block-list-issue-description-link">
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
class="break-link"
>
{{ instance.uri }}
</a>
</div>
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block report-block-dast-code prepend-top-10 report-block-issue-code"
>{{ instance.evidence }}</pre>
</expand-button>
</div>
</li>
</ul>
</slot>
<div slot="footer">
</div>
</modal>
</div>
</template>
......@@ -96,7 +96,9 @@
return this.status === 'success';
},
statusIconName() {
if (this.loadingFailed || this.unresolvedIssues.length) {
if (this.loadingFailed ||
this.unresolvedIssues.length ||
this.neutralIssues.length) {
return 'warning';
}
return 'success';
......@@ -221,7 +223,7 @@
<button
v-if="allIssues.length && !isFullReportVisible"
type="button"
class="btn-link btn-blank prepend-left-10 js-expand-full-list"
class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
@click="openFullReport"
>
{{ s__("ciReport|Show complete code vulnerabilities report") }}
......
.pipeline-tab-content {
.space-children,
.space-children > span {
display: flex;
}
.media {
align-items: center;
}
}
.report-block-container {
border-top: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
}
.report-block-dast-code {
margin-left: 26px;
// Clean MR widget CSS
line-height: 20px;
}
.report-block-list {
list-style: none;
padding: 0 1px;
margin: 0;
line-height: $code_line_height;
.btn-open-modal {
padding: 0 5px 4px;
}
}
.report-block-list-item {
display: flex;
}
.report-block-list-icon {
display: flex;
.report-block-list-item-modal {
display: flex;
flex-wrap: wrap;
}
.failed .report-block-icon {
&.failed {
color: $red-500;
}
.success .report-block-icon {
&.success {
color: $green-500;
}
.neutral .report-block-icon {
&.neutral {
color: $theme-gray-700;
}
}
.report-block-icon {
margin: -5px 4px 0 0;
fill: currentColor;
}
.report-block-list-issue {
display: flex;
align-items: flex-start;
align-content: flex-start;
}
.pipeline-tab-content {
.space-children,
.space-children > * {
display: flex;
.report-block-list-issue-description {
align-content: space-around;
align-items: flex-start;
flex-wrap: wrap;
display: flex;
align-self: center;
}
.report-block {
.break-link {
word-wrap: break-word;
word-break: break-all;
}
}
.media {
align-items: center;
.report-block-issue-code {
width: $modal-lg - 70px;
}
.modal-security-report-dast {
.modal-dialog {
width: $modal-lg;
}
}
\ No newline at end of file
// TODO remove this when gl_modal support not rendering the footer
.modal-footer {
display: none;
}
}
......@@ -52,8 +52,22 @@ module Geo
private
def fetch_repository(redownload)
log_info("Trying to fetch #{type}")
update_registry!(started_at: DateTime.now)
if redownload
log_info("Redownloading #{type}")
fetch_geo_mirror(build_temporary_repository)
set_temp_repository_as_main
else
ensure_repository
fetch_geo_mirror(repository)
end
end
def retry_count
registry.public_send("#{type}_retry_count") || 0 # rubocop:disable GitlabSecurity/PublicSend
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
def should_be_retried?
......@@ -68,10 +82,6 @@ module Geo
(RETRY_BEFORE_REDOWNLOAD..RETRY_LIMIT) === retry_count
end
def sync_repository
raise NotImplementedError, 'This class should implement sync_repository method'
end
def current_node
::Gitlab::Geo.current_node
end
......
......@@ -5,32 +5,21 @@ module Geo
private
def sync_repository(redownload = false)
fetch_project_repository(redownload)
expire_repository_caches
end
def fetch_project_repository(redownload)
log_info('Trying to fetch project repository')
update_registry!(started_at: DateTime.now)
if redownload
log_info('Redownloading repository')
fetch_geo_mirror(build_temporary_repository)
set_temp_repository_as_main
else
project.ensure_repository
fetch_geo_mirror(project.repository)
end
fetch_repository(redownload)
update_gitattributes
update_registry!(finished_at: DateTime.now, attrs: { last_repository_sync_failure: nil })
log_info('Finished repository sync',
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
mark_sync_as_successful
rescue Gitlab::Shell::Error,
Gitlab::Git::RepositoryMirroring::RemoteError => e
fail_registry!('Error syncing repository', e)
# In some cases repository does not exist, the only way to know about this is to parse the error text.
# If it does not exist we should consider it as successfully downloaded.
if e.message.include? Gitlab::GitAccess::ERROR_MESSAGES[:no_repo]
log_info('Repository is not found, marking it as successfully synced')
mark_sync_as_successful
else
fail_registry!('Error syncing repository', e)
end
rescue Gitlab::Git::Repository::NoRepository => e
log_info('Setting force_to_redownload flag')
fail_registry!('Invalid repository', e, force_to_redownload_repository: true)
......@@ -39,6 +28,15 @@ module Geo
project.repository.after_create
ensure
clean_up_temporary_repository if redownload
expire_repository_caches
end
def mark_sync_as_successful
update_registry!(finished_at: DateTime.now, attrs: { last_repository_sync_failure: nil })
log_info('Finished repository sync',
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
end
def expire_repository_caches
......@@ -54,15 +52,15 @@ module Geo
project.repository
end
def ensure_repository
project.ensure_repository
end
# Update info/attributes file using the contents of .gitattributes file from the default branch
def update_gitattributes
return if project.default_branch.nil?
repository.copy_gitattributes(project.default_branch)
end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end
end
......@@ -5,30 +5,16 @@ module Geo
private
def sync_repository(redownload = false)
fetch_wiki_repository(redownload)
end
def fetch_wiki_repository(redownload)
log_info('Fetching wiki repository')
update_registry!(started_at: DateTime.now)
if redownload
log_info('Redownloading wiki')
fetch_geo_mirror(build_temporary_repository)
set_temp_repository_as_main
else
project.wiki.ensure_repository
fetch_geo_mirror(project.wiki.repository)
end
fetch_repository(redownload)
mark_sync_as_successful
rescue Gitlab::Git::RepositoryMirroring::RemoteError,
Gitlab::Shell::Error,
ProjectWiki::CouldNotCreateWikiError => e
# In some cases repository does not exists, the only way to know about this is to parse the error text.
# If it does not exist we should consider it as successfuly downloaded.
# In some cases repository does not exist, the only way to know about this is to parse the error text.
# If it does not exist we should consider it as successfully downloaded.
if e.message.include? Gitlab::GitAccess::ERROR_MESSAGES[:no_repo]
log_info('Repository is not found, marking it as successfully synced')
log_info('Wiki repository is not found, marking it as successfully synced')
mark_sync_as_successful
else
fail_registry!('Error syncing wiki repository', e)
......@@ -48,6 +34,10 @@ module Geo
project.wiki.repository
end
def ensure_repository
project.wiki.ensure_repository
end
def mark_sync_as_successful
update_registry!(finished_at: DateTime.now, attrs: { last_wiki_sync_failure: nil })
......@@ -55,9 +45,5 @@ module Geo
update_delay_s: update_delay_in_seconds,
download_time_s: download_time_in_seconds)
end
def retry_count
registry.public_send("#{type}_retry_count") || -1 # rubocop:disable GitlabSecurity/PublicSend
end
end
end
......@@ -118,7 +118,7 @@
%td= @stats[:merge_requests_merged][index]
%td= @stats[:total_events][index]
-# haml-lint:disable InlineJavaScript
%script#js-analytics-data{ type: "application/json" }
- data = {}
- data[:labels] = @users.map(&:name)
......
= webpack_bundle_tag 'add_gitlab_slack_application'
-# haml-lint:disable InlineJavaScript
%script#js-add-gitlab-slack-application-entry-data{ type: "application/json" }
= add_to_slack_data(@projects)
......
---
title: Allow clicking on Staged Files in WebIDE to open them in the Editor
merge_request:
author:
type: other
---
title: Improve security reports to handle big links and to work on mobile devices
merge_request: 4671
author:
type: fixed
---
title: Mark empty repos as synced in Geo
merge_request: 4757
author:
type: fixed
......@@ -5,8 +5,6 @@ describe Geo::BaseSyncService do
subject { described_class.new(project) }
it_behaves_like 'geo base sync execution'
describe '#lease_key' do
it 'returns a key in the correct pattern' do
allow(described_class).to receive(:type) { :wiki }
......
......@@ -115,6 +115,19 @@ describe Geo::RepositorySyncService do
expect(Geo::ProjectRegistry.last.repository_retry_count).to eq(1)
end
it 'marks sync as successful if no repository found' do
registry = create(:geo_project_registry, project: project)
allow(repository).to receive(:fetch_as_mirror)
.with(url_to_repo, remote_name: 'geo', forced: true)
.and_raise(Gitlab::Shell::Error.new(Gitlab::GitAccess::ERROR_MESSAGES[:no_repo]))
subject.execute
expect(registry.reload.resync_repository).to be false
expect(registry.reload.last_repository_successful_sync_at).not_to be nil
end
context 'tracking database' do
it 'creates a new registry if does not exists' do
expect { subject.execute }.to change(Geo::ProjectRegistry, :count).by(1)
......@@ -205,7 +218,7 @@ describe Geo::RepositorySyncService do
it 'tries to fetch repo' do
create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD - 1)
expect_any_instance_of(described_class).to receive(:fetch_project_repository).with(false)
expect(subject).to receive(:sync_repository).with(no_args)
subject.execute
end
......@@ -221,7 +234,7 @@ describe Geo::RepositorySyncService do
it 'tries to redownload repo' do
create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD + 1)
expect(subject).to receive(:fetch_project_repository).with(true).and_call_original
expect(subject).to receive(:sync_repository).with(true).and_call_original
expect(subject.gitlab_shell).to receive(:mv_repository).exactly(2).times.and_call_original
expect(subject.gitlab_shell).to receive(:remove_repository).exactly(3).times.and_call_original
......@@ -242,7 +255,7 @@ describe Geo::RepositorySyncService do
force_to_redownload_repository: true
)
expect_any_instance_of(described_class).to receive(:fetch_project_repository).with(true)
expect(subject).to receive(:sync_repository).with(true)
subject.execute
end
......
......@@ -4,11 +4,11 @@ shared_examples 'geo base sync execution' do
context 'when can acquire exclusive lease' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { 12345 }
exclusive_lease = double(:exclusive_lease, try_obtain: 12345)
expect(subject).to receive(:exclusive_lease).and_return(exclusive_lease)
end
it 'executes the synchronization' do
subject.class.type ||= :wiki
expect(subject).to receive(:sync_repository)
subject.execute
......@@ -17,7 +17,8 @@ shared_examples 'geo base sync execution' do
context 'when exclusive lease is not acquired' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
exclusive_lease = double(:exclusive_lease, try_obtain: nil)
expect(subject).to receive(:exclusive_lease).and_return(exclusive_lease)
end
it 'is does not execute synchronization' do
......
......@@ -48,6 +48,8 @@ module API
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
......
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?
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
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)
......@@ -10,21 +10,40 @@ WHITELIST = [
'vendor/assets/javascripts/jasmine-jquery.js'
].freeze
`git remote add canonical-ee https://gitlab.com/gitlab-org/gitlab-ee.git`
`git remote add canonical-ce https://gitlab.com/gitlab-org/gitlab-ce.git`
`git fetch canonical-ee master --quiet`
def run_git_command(cmd)
puts "=> Running `git #{cmd}`"
`git #{cmd}`
end
run_git_command("remote add canonical-ee https://gitlab.com/gitlab-org/gitlab-ee.git")
run_git_command("remote add canonical-ce https://gitlab.com/gitlab-org/gitlab-ce.git")
run_git_command("fetch canonical-ee master --quiet")
new_files_in_this_branch_not_at_the_ee_top_level =
`git diff canonical-ee/master...HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2`.lines.map(&:strip)
run_git_command("diff canonical-ee/master...HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2").lines.map(&:strip)
ce_repo_url = ENV['CI_REPOSITORY_URL'].sub('gitlab-ee', 'gitlab-ce')
ce_repo_url = ENV.fetch('CI_REPOSITORY_URL', 'https://gitlab.com/gitlab-org/gitlab-ce.git').sub('gitlab-ee', 'gitlab-ce')
current_branch = ENV.fetch('CI_COMMIT_REF_NAME', `git rev-parse --abbrev-ref HEAD`).strip
ce_branch_name = current_branch.sub(/(\Aee\-|\-ee\z)/, '')
minimal_ce_branch_name = current_branch.sub(/(\Aee\-|\-ee\z)/, '')
ls_remote_output = run_git_command("ls-remote #{ce_repo_url} \"*#{minimal_ce_branch_name}*\"")
remote_to_fetch = 'canonical-ce'
branch_to_fetch = 'master'
if ls_remote_output.include?(minimal_ce_branch_name)
remote_to_fetch = ce_repo_url
branch_to_fetch = ls_remote_output.split("refs/heads/").last.strip
puts
puts "💪 We found the branch '#{branch_to_fetch}' in the #{ce_repo_url} repository. We will fetch it."
else
puts "⚠️ We did not find a branch that would match the current '#{current_branch}' branch in the #{ce_repo_url} repository. We will fetch 'master' instead."
puts "ℹ️ If you have a CE branch for the current branch, make sure that its name includes '#{minimal_ce_branch_name}'."
end
`git fetch #{ce_repo_url} #{ce_branch_name} --quiet` || `git fetch canonical-ce 'master' --quiet`
run_git_command("fetch #{remote_to_fetch} #{branch_to_fetch} --quiet")
ee_specific_files_in_ce_master_not_at_the_ee_top_level =
`git diff FETCH_HEAD..HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2`.lines.map(&:strip)
run_git_command("diff FETCH_HEAD..HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2").lines.map(&:strip)
new_ee_specific_files_not_at_the_ee_top_level =
new_files_in_this_branch_not_at_the_ee_top_level & ee_specific_files_in_ce_master_not_at_the_ee_top_level
......@@ -34,12 +53,21 @@ new_ee_specific_files_not_at_the_ee_top_level.each do |file|
next if WHITELIST.any? { |pattern| Dir.glob(pattern).include?(file) }
puts
puts "* #{file} is EE-specific and should be moved to ee/#{file}:"
puts "* 💥 #{file} is EE-specific and should be moved to ee/#{file}: 💥"
puts " => git mv #{file} ee/#{file}"
status = 1
end
`git remote remove canonical-ee`
`git remote remove canonical-ce`
if status.zero?
puts
puts "🎉 All good, congrats! 🎉"
end
run_git_command("remote remove canonical-ee")
run_git_command("remote remove canonical-ce")
puts
puts "ℹ️ For more information on the why and how of this job, see https://docs.gitlab.com/ee/development/ee_features.html#detection-of-ee-only-files"
puts
exit(status)
......@@ -18,7 +18,7 @@ describe MergeRequestsFinder do
let(:project4) { create(:project, :public, group: subgroup) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
......@@ -80,6 +80,22 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
it 'filters by source branch' do
params = { source_branch: merge_request2.source_branch }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request2)
end
it 'filters by target branch' do
params = { target_branch: merge_request2.target_branch }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request2)
end
context 'filtering by group milestone' do
let!(:group) { create(:group, :public) }
let(:group_milestone) { create(:milestone, group: group) }
......
import Vue from 'vue';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
......@@ -33,6 +34,16 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.discardFileChanges).toHaveBeenCalled();
});
it('opens a closed file in the editor when clicking the file path', () => {
spyOn(vm, 'openFileInEditor').and.callThrough();
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
expect(vm.openFileInEditor).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled();
});
describe('computed', () => {
describe('iconName', () => {
it('returns modified when not a tempFile', () => {
......
......@@ -93,6 +93,9 @@ describe('TimelineHeaderItemComponent', () => {
timeframeIndex,
timeframeItem,
});
vm.currentYear = mockTimeframe[timeframeIndex].getFullYear();
vm.currentMonth = mockTimeframe[timeframeIndex].getMonth() + 1;
expect(vm.timelineHeaderClass).toBe('label-dark');
});
});
......
import Vue from 'vue';
import modal from 'ee/vue_shared/security_reports/components/dast_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('mr widget modal', () => {
let vm;
let Modal;
beforeEach(() => {
Modal = Vue.extend(modal);
vm = mountComponent(Modal, {
title: 'Title',
targetId: 'targetId',
instances: [{
uri: 'uri',
method: 'GET',
evidence: 'evidence',
}],
description: 'Description!',
});
});
afterEach(() => {
vm.$destroy();
});
it('renders a title', () => {
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toEqual('Title');
});
it('renders the target id', () => {
expect(vm.$el.getAttribute('id')).toEqual('targetId');
});
it('renders the description', () => {
expect(vm.$el.querySelector('.modal-body').textContent).toContain('Description!');
});
it('renders list of instances', () => {
const instance = vm.$el.querySelector('.modal-body li').textContent;
expect(instance).toContain('uri');
expect(instance).toContain('GET');
expect(instance).toContain('evidence');
});
});
......@@ -159,5 +159,20 @@ describe('Report issues', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name);
expect(vm.$el.textContent).toContain(parsedDast[0].priority);
});
it('opens modal with more information and list of instances', (done) => {
vm.$el.querySelector('.js-modal-dast').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toEqual('Low (Medium): Absence of Anti-CSRF Tokens');
expect(vm.$el.querySelector('.modal-body').textContent).toContain('No Anti-CSRF tokens were found in a HTML submission form.');
const instance = vm.$el.querySelector('.modal-body li').textContent;
expect(instance).toContain('http://192.168.32.236:3001/explore?sort=latest_activity_desc');
expect(instance).toContain('GET');
done();
});
});
});
});
......@@ -192,7 +192,7 @@ describe('Report section', () => {
it('should show the report by default', () => {
expect(
vm.$el.querySelectorAll('.report-block-list .report-block-list-item').length,
vm.$el.querySelectorAll('.report-block-list .report-block-list-issue').length,
).toEqual(codequalityParsedIssues.length);
});
});
......
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
......@@ -151,6 +151,26 @@ describe API::MergeRequests do
expect(json_response.first['id']).to eq(merge_request3.id)
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
expect(json_response.length).to eq(2)
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request_closed.id, merge_request_merged.id)
end
end
context 'target_branch param' do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
expect(json_response.length).to eq(2)
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request_closed.id, merge_request_merged.id)
end
end
context 'search params' do
before do
merge_request.update(title: 'Search title', description: 'Search description')
......@@ -427,6 +447,26 @@ describe API::MergeRequests do
expect(response_dates).to eq(response_dates.sort)
end
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
expect(json_response.length).to eq(2)
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request_closed.id, merge_request_merged.id)
end
end
context 'target_branch param' do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
expect(json_response.length).to eq(2)
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request_closed.id, merge_request_merged.id)
end
end
end
end
......
......@@ -702,7 +702,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
......
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