Commit 23d44f5b authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-06-08' into 'master'

CE upstream - 2018-06-08 12:25 UTC

Closes #5974, gitlab-qa#251, and gitlab-qa#258

See merge request gitlab-org/gitlab-ee!6058
parents 346398ad 8f211750
......@@ -434,7 +434,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.101.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -307,7 +307,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.100.0)
gitaly-proto (0.101.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -1073,7 +1073,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.100.0)
gitaly-proto (~> 0.101.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......
......@@ -142,4 +142,3 @@ export default {
</div>
</div>
</template>
......@@ -69,9 +69,10 @@ export default () => {
gl.diffNotesCompileComponents();
if (!hasVueMRDiscussionsCookie()) {
const resolveCountAppEl = document.querySelector('#resolve-count-app');
if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) {
new Vue({
el: '#resolve-count-app',
el: resolveCountAppEl,
components: {
'resolve-count': ResolveCount
},
......
......@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
import ResizablePanel from '../resizable_panel.vue';
export default {
directives: {
......@@ -14,6 +15,7 @@ export default {
Icon,
PipelinesList,
JobsDetail,
ResizablePanel,
},
computed: {
...mapState(['rightPane']),
......@@ -40,12 +42,16 @@ export default {
<div
class="multi-file-commit-panel ide-right-sidebar"
>
<div
class="multi-file-commit-panel-inner"
<resizable-panel
v-if="rightPane"
class="multi-file-commit-panel-inner"
:collapsible="false"
:initial-width="350"
:min-size="350"
side="right"
>
<component :is="rightPane" />
</div>
</resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li>
......
<script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
......@@ -50,7 +48,7 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (oldVal.key !== this.file.key) {
this.initMonaco();
this.initEditor();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
......@@ -84,15 +82,10 @@ export default {
this.editor.dispose();
},
mounted() {
if (this.editor && monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.editor = Editor.create(monaco);
this.initMonaco();
});
if (!this.editor) {
this.editor = Editor.create();
}
this.initEditor();
},
methods: {
...mapActions([
......@@ -105,7 +98,7 @@ export default {
'updateViewer',
'removePendingTab',
]),
initMonaco() {
initEditor() {
if (this.shouldHideEditor) return;
this.editor.clearEditor();
......@@ -118,7 +111,7 @@ export default {
this.createEditorInstance();
})
.catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
......
import { editor as monacoEditor, Uri } from 'monaco-editor';
import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
constructor(monaco, file, head = null) {
this.monaco = monaco;
constructor(file, head = null) {
this.disposable = new Disposable();
this.file = file;
this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
(this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.path}`),
new Uri(false, false, `original/${this.path}`),
)),
(this.model = this.monaco.editor.createModel(
(this.model = monacoEditor.createModel(
this.content,
undefined,
new this.monaco.Uri(null, null, this.path),
new Uri(false, false, this.path),
)),
);
if (this.file.mrChange) {
this.disposable.add(
(this.baseModel = this.monaco.editor.createModel(
(this.baseModel = monacoEditor.createModel(
this.file.baseRaw,
undefined,
new this.monaco.Uri(null, null, `target/${this.path}`),
new Uri(false, false, `target/${this.path}`),
)),
);
}
......
......@@ -3,8 +3,7 @@ import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
constructor(monaco) {
this.monaco = monaco;
constructor() {
this.disposable = new Disposable();
this.models = new Map();
}
......@@ -22,7 +21,7 @@ export default class ModelManager {
return this.getModel(file.key);
}
const model = new Model(this.monaco, file, head);
const model = new Model(file, head);
this.models.set(model.path, model);
this.disposable.add(model);
......
/* global monaco */
import { Range } from 'monaco-editor';
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
......@@ -16,7 +16,7 @@ export const getDiffChangeType = change => {
};
export const getDecorator = change => ({
range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
range: new Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
......
import _ from 'underscore';
import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
......@@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
function setupMonacoTheme() {
monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
monacoEditor.setTheme('gitlab');
}
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
......@@ -17,24 +23,22 @@ export const clearDomElement = el => {
};
export default class Editor {
static create(monaco) {
if (this.editorInstance) return this.editorInstance;
this.editorInstance = new Editor(monaco);
static create() {
if (!this.editorInstance) {
this.editorInstance = new Editor();
}
return this.editorInstance;
}
constructor(monaco) {
this.monaco = monaco;
constructor() {
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
this.modelManager = new ModelManager(this.monaco);
this.modelManager = new ModelManager();
this.decorationsController = new DecorationsController(this);
this.setupMonacoTheme();
setupMonacoTheme();
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
......@@ -46,7 +50,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.create(domElement, {
(this.instance = monacoEditor.create(domElement, {
...defaultEditorOptions,
})),
(this.dirtyDiffController = new DirtyDiffController(
......@@ -66,7 +70,7 @@ export default class Editor {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
(this.instance = monacoEditor.createDiffEditor(domElement, {
...defaultEditorOptions,
quickSuggestions: false,
occurrencesHighlight: false,
......@@ -122,17 +126,11 @@ export default class Editor {
modified: model.getModel(),
});
this.monaco.editor.createDiffNavigator(this.instance, {
monacoEditor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
......@@ -200,7 +198,7 @@ export default class Editor {
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
return monacoKeyMod ? KeyCode[key] : KeyMod[key];
};
keymap.forEach(command => {
......
import monacoContext from 'monaco-editor/dev/vs/loader';
monacoContext.require.config({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
});
// ignore CDN config and use local assets path for service worker which cannot be cross-domain
const relativeRootPath = (gon && gon.relative_url_root) || '';
const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
// eslint-disable-next-line no-underscore-dangle
window.__monaco_context__ = monacoContext;
export default monacoContext.require;
......@@ -79,37 +79,37 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
[s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
[s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')],
[s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')],
[s__('Timeago|a day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
[s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
[s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
[s__('Timeago|a week ago'), s__('Timeago|1 week remaining')],
[s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
[s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
[s__('Timeago|a month ago'), s__('Timeago|1 month remaining')],
[s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
[s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
[s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
};
const locale = function getLocale(number, index) {
return [
[s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
[s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')],
[s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')],
[s__('Timeago|a day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
[s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
[s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
[s__('Timeago|a week ago'), s__('Timeago|in 1 week')],
[s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
[s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
[s__('Timeago|a month ago'), s__('Timeago|in 1 month')],
[s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
[s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
[s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
};
......
......@@ -147,6 +147,7 @@ document.addEventListener('DOMContentLoaded', () => {
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover',
boundary: 'viewport',
placement(tip, el) {
return $(el).data('placement') || 'bottom';
},
......
......@@ -107,7 +107,7 @@ code {
background-color: $red-100;
border-radius: 3px;
.code & {
.code > & {
background-color: inherit;
padding: unset;
}
......@@ -233,6 +233,13 @@ table {
}
}
.card-header {
h3.card-title,
h4.card-title {
margin-top: 0;
}
}
.nav-tabs {
// Override bootstrap's default border
border-bottom: 0;
......
......@@ -497,6 +497,10 @@ fieldset[disabled] .btn,
}
}
[readonly] {
cursor: default;
}
.btn-no-padding {
padding: 0;
}
......@@ -174,11 +174,6 @@ $border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
* Override Bootstrap 4 variables
*/
$secondary: $gray-light;
/*
* UI elements
*/
......@@ -846,3 +841,14 @@ Prometheus
$prometheus-table-row-highlight-color: $theme-gray-100;
$priority-label-empty-state-width: 114px;
/*
* Override Bootstrap 4 variables
*/
$secondary: $gray-light;
$input-disabled-bg: $gray-light;
$input-border-color: $theme-gray-200;
$input-color: $gl-text-color;
$font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font;
......@@ -18,7 +18,8 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
.form-control:hover {
.form-control:hover,
:not[readonly] {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
......
......@@ -60,7 +60,7 @@ module IconsHelper
def spinner(text = nil, visible = false)
css_class = 'loading'
css_class << ' hidden' unless visible
css_class << ' hide' unless visible
content_tag :div, class: css_class do
icon('spinner spin') + text
......
......@@ -167,7 +167,7 @@ module IssuablesHelper
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
output.html_safe
end
......
......@@ -13,6 +13,7 @@ class Group < Namespace
include GroupDescendant
include TokenAuthenticatable
include WithUploads
include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
......@@ -28,7 +29,11 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
# Overridden on another method
# Left here just to be dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
......@@ -90,6 +95,15 @@ class Group < Namespace
end
end
# Overrides notification_settings has_many association
# This allows to apply notification settings from parent groups
# to child groups and projects.
def notification_settings
source_type = self.class.base_class.name
NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
end
def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
......@@ -227,6 +241,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id)
end
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
end
end
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
......
class NotificationRecipient
include Gitlab::Utils::StrongMemoize
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
......@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global?
group_setting = @group && user.notification_settings_for(@group)
group_setting = closest_non_global_group_notification_settting
return group_setting unless group_setting.nil? || group_setting.global?
return group_setting unless group_setting.nil?
user.global_notification_setting
end
# Returns the notificaton_setting of the lowest group in hierarchy with non global level
def closest_non_global_group_notification_settting
return unless @group
return if indexed_group_notification_settings.empty?
notification_setting = nil
@group.self_and_ancestors_ids.each do |id|
notification_setting = indexed_group_notification_settings[id]
break if notification_setting
end
notification_setting
end
def indexed_group_notification_settings
strong_memoize(:indexed_group_notification_settings) do
@group.notification_settings.where(user_id: user.id)
.where.not(level: NotificationSetting.levels[:global])
.index_by(&:source_id)
end
end
end
......@@ -7,5 +7,4 @@
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
.form-actions
= form.submit 'Save', class: 'btn btn-save'
= form.submit 'Save', class: 'btn btn-save'
......@@ -3,5 +3,5 @@
Groups with access to
%strong= @project.name
%span.badge.badge-pill= group_links.size
%ul.content-list
%ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
.card-header
%h3.card-title
.card-header.bg-white
%h3.card-title.mb-0
Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
......
- enabled = Gitlab.config.mattermost.enabled
.card
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Mattermost after setting up this service,
by entering
%kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
.info-well
.well-segment
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Mattermost after setting up this service,
by entering
%kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.card
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Slack after setting up this service,
by entering
%kbd.inline /&lt;command&gt; help
- unless @service.template?
%p To setup this service:
%ul.list-unstyled.indent-list
%li
1.
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
= icon('external-link')
in your Slack team with these options:
.info-well
.well-segment
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Slack after setting up this service,
by entering
%kbd.inline /&lt;command&gt; help
- unless @service.template?
%p To setup this service:
%ul.list-unstyled.indent-list
%li
1.
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
= icon('external-link')
in your Slack team with these options:
%hr
%hr
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#url', class: 'input-group-text')
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#url', class: 'input-group-text')
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block POST
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block POST
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block Show this command in the autocomplete list
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block Show this command in the autocomplete list
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text')
%hr
%hr
%ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
%ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
......@@ -7,7 +7,7 @@
= f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hidden js-secret-value", placeholder: 'xEeFCaDAB89'
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
%p.form-text.text-muted The secure token used by the Runner to checkout the project
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
......
......@@ -3,8 +3,9 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
.card
= markdown @service.help
.info-well
.well-segment
= markdown @service.help
.service-settings
- if @service.show_active_box?
......@@ -15,25 +16,24 @@
- if @service.configurable_events.present?
.form-group.row
= form.label :url, "Trigger", class: 'col-form-label col-sm-2'
.col-sm-2.text-right Trigger
.col-sm-10
- @service.configurable_events.each do |event|
%div
= form.check_box service_event_field_name(event), class: 'float-left'
.prepend-left-20
= form.label service_event_field_name(event), class: 'list-label' do
.form-group
.form-check
= form.check_box service_event_field_name(event), class: 'form-check-input'
= form.label service_event_field_name(event), class: 'form-check-label' do
%strong
= event.humanize
- field = @service.event_field(event)
- field = @service.event_field(event)
- if field
%p
- if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.light
= @service.class.event_description(event)
%p.text-muted
= @service.class.event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
......
.dropdown.prepend-left-10#js-add-list
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, board.parent)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
......@@ -5,16 +5,16 @@
%li.member.group_member{ id: dom_id }
%span.list-item-name
= group_icon(group, class: "avatar s40", alt: '')
%strong
= link_to group.full_name, group_path(group)
.cgray
Given access #{time_ago_with_tooltip(group_link.created_at)}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.user-info
= link_to group.full_name, group_path(group), class: 'member'
.cgray
Given access #{time_ago_with_tooltip(group_link.created_at)}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do
= form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
......
......@@ -5,6 +5,6 @@
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
%span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
---
title: Use the default strings of timeago.js for timeago
merge_request: 19350
author: Takuya Noguchi
type: other
---
title: Apply notification settings level of groups to all child objects
merge_request:
author:
type: changed
......@@ -534,3 +534,9 @@
:why: https://github.com/squaremo/bitsyntax-js/blob/master/LICENSE-MIT
:versions: []
:when: 2018-02-20 22:20:25.958123000 Z
- - :approve
- "@webassemblyjs/ieee754"
- :who: Mike Greiling
:why: https://github.com/xtuc/webassemblyjs/blob/master/LICENSE
:versions: []
:when: 2018-06-08 05:30:56.764116000 Z
......@@ -29,14 +29,14 @@
label: Pod average
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
y_label: "HTTP Errors"
required_metrics:
- nginx_upstream_responses_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100'
label: 5xx Errors
unit: "%"
- group: Response metrics (HA Proxy)
priority: 10
metrics:
......
......@@ -4,8 +4,8 @@ const glob = require('glob');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ROOT_PATH = path.resolve(__dirname, '..');
......@@ -181,15 +181,7 @@ module.exports = {
name: '[name].[hash:8].[ext]',
},
},
{
test: /monaco-editor\/\w+\/vs\/loader\.js$/,
use: [
{ loader: 'exports-loader', options: 'l.global' },
{ loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' },
],
},
],
noParse: [/monaco-editor\/\w+\/vs\//],
},
optimization: {
......@@ -239,6 +231,9 @@ module.exports = {
// enable vue-loader to use existing loader rules for other module types
new VueLoaderPlugin(),
// automatically configure monaco editor web workers
new MonacoWebpackPlugin(),
// prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/),
......@@ -248,29 +243,6 @@ module.exports = {
jQuery: 'jquery',
}),
// copy pre-compiled vendor libraries verbatim
new CopyWebpackPlugin([
{
from: path.join(
ROOT_PATH,
`node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`
),
to: 'monaco-editor/vs',
transform: function(content, path) {
if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) {
return (
'(function(){\n' +
'var define = this.define, require = this.require;\n' +
'window.define = define; window.require = require;\n' +
content +
'\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));'
);
}
return content;
},
},
]),
// compression can require a lot of compute time and is disabled in CI
IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
......
......@@ -162,13 +162,13 @@ such as Trello, JIRA, etc.
## Webhooks
Configure [webhooks](project/integrations/webhooks.html) to listen for
Configure [webhooks](project/integrations/webhooks.md) to listen for
specific events like pushes, issues or merge requests. GitLab will send a
POST request with data to the webhook URL.
## API
Automate GitLab via [API](../api/README.html).
Automate GitLab via [API](../api/README.md).
## Git and GitLab
......
......@@ -35,7 +35,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
Line-breaks, or soft returns, are rendered if you end a line with two or more spaces:
[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
[//]: # (They are needed for the Markdown text to render correctly.)
......@@ -198,7 +198,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-
With inline diffs tags you can display {+ additions +} or [- deletions -].
The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
The wrapping tags can be either curly braces or square brackets: [+ additions +] or {- deletions -}.
Examples:
......@@ -229,7 +229,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
......@@ -239,7 +239,7 @@ Sometimes you want to :monkey: around a bit and add some :star2: to your :speech
You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
......@@ -407,7 +407,7 @@ Examples:
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
Becomes:
Become:
`#F00`
`#F00A`
......@@ -484,14 +484,14 @@ Alt-H2
All Markdown-rendered headers automatically get IDs, except in comments.
On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
The IDs are generated from the content of the header according to the following rules:
1. All text is converted to lowercase
1. All non-word text (e.g., punctuation, HTML) is removed
1. All spaces are converted to hyphens
1. Two or more hyphens in a row are converted to one
1. All text is converted to lowercase.
1. All non-word text (e.g., punctuation, HTML) is removed.
1. All spaces are converted to hyphens.
1. Two or more hyphens in a row are converted to one.
1. If a header with the same ID has already been generated, a unique
incrementing number is appended, starting at 1.
......@@ -519,6 +519,8 @@ Note that the Emoji processing happens before the header IDs are generated, so t
### Emphasis
Examples:
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
......@@ -529,6 +531,8 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
Become:
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
......@@ -539,6 +543,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
### Lists
Examples:
```no-highlight
1. First ordered list item
2. Another item
......@@ -552,6 +558,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
+ Or pluses
```
Become:
1. First ordered list item
2. Another item
* Unordered sub-list.
......@@ -566,6 +574,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
If a list item contains multiple paragraphs,
each subsequent paragraph should be indented with four spaces.
Example:
```no-highlight
1. First ordered list item
......@@ -573,6 +583,8 @@ each subsequent paragraph should be indented with four spaces.
2. Another item
```
Becomes:
1. First ordered list item
Second paragraph of first item.
......@@ -581,6 +593,8 @@ each subsequent paragraph should be indented with four spaces.
If the second paragraph isn't indented with four spaces,
the second list item will be incorrectly labeled as `1`.
Example:
```no-highlight
1. First ordered list item
......@@ -588,6 +602,8 @@ the second list item will be incorrectly labeled as `1`.
2. Another item
```
Becomes:
1. First ordered list item
Second paragraph of first item.
......@@ -625,6 +641,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
### Images
Examples:
Here's our logo (hover to see the title text):
Inline-style:
......@@ -635,6 +653,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
[logo]: img/markdown_logo.png
Become:
Here's our logo:
Inline-style:
......@@ -649,6 +669,8 @@ Reference-style:
### Blockquotes
Examples:
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
......@@ -658,6 +680,8 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
Become:
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
......@@ -671,6 +695,8 @@ You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements.
Examples:
```no-highlight
<dl>
<dt>Definition list</dt>
......@@ -681,6 +707,8 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
</dl>
```
Become:
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
......@@ -715,6 +743,8 @@ These details will remain hidden until expanded.
### Horizontal Rule
Examples:
```
Three or more...
......@@ -731,6 +761,8 @@ ___
Underscores
```
Become:
Three or more...
---
......@@ -751,6 +783,8 @@ My basic recommendation for learning how line breaks work is to experiment and d
Here are some things to try out:
Examples:
```
Here's a line for us to start with.
......@@ -765,6 +799,8 @@ This line is *on its own line*, because the previous line ends with two spaces.
spaces.
```
Become:
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
......@@ -781,6 +817,8 @@ spaces.
Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
Example:
```
| header 1 | header 2 |
| -------- | -------- |
......@@ -788,7 +826,7 @@ Tables aren't part of the core Markdown spec, but they are part of GFM and Markd
| cell 3 | cell 4 |
```
Code above produces next output:
Becomes:
| header 1 | header 2 |
| -------- | -------- |
......@@ -799,7 +837,9 @@ Code above produces next output:
The row of dashes between the table header and body must have at least three dashes in each column.
By including colons in the header row, you can align the text within that column:
By including colons in the header row, you can align the text within that column.
Example:
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
......@@ -808,6 +848,8 @@ By including colons in the header row, you can align the text within that column
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
```
Becomes:
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
......@@ -815,11 +857,15 @@ By including colons in the header row, you can align the text within that column
### Footnotes
Example:
```
You can add footnotes to your text as follows.[^2]
[^2]: This is my awesome footnote.
```
Becomes:
You can add footnotes to your text as follows.[^2]
## Wiki-specific Markdown
......
......@@ -14,7 +14,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 |
## Configuring NGINX ingress monitoring
......
......@@ -34,9 +34,14 @@ anything that is set at Global Settings.
![notification settings](img/notification_group_settings.png)
Group Settings are taking precedence over Global Settings but are on a level below Project Settings.
Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
```
Group < Subgroup < Project
```
This means that you can set a different level of notifications per group while still being able
to have a finer level setting per project.
to have a finer level setting per project or subgroup.
Organization like this is suitable for users that belong to different groups but don't have the
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
......
......@@ -112,18 +112,31 @@ module Backup
end
end
def local_restore_custom_hooks(project, dir)
path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
path_to_repo(project)
end
cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
output, status = Gitlab::Popen.popen(cmd)
unless status.zero?
progress_warn(project, cmd.join(' '), output)
end
end
def gitaly_restore_custom_hooks(project, dir)
custom_hooks_path = path_to_tars(project, dir)
Gitlab::GitalyClient::RepositoryService.new(project.repository)
.restore_custom_hooks(custom_hooks_path)
end
def restore_custom_hooks(project)
# TODO: Need to find a way to do this for gitaly
# Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/1195
in_path(path_to_tars(project)) do |dir|
path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
path_to_repo(project)
end
cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
output, status = Gitlab::Popen.popen(cmd)
unless status.zero?
progress_warn(project, cmd.join(' '), output)
gitaly_migrate(:restore_custom_hooks) do |is_enabled|
if is_enabled
local_restore_custom_hooks(project, dir)
else
gitaly_restore_custom_hooks(project, dir)
end
end
end
end
......
......@@ -62,7 +62,7 @@ module Gitlab
end
def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
Gitlab::Git::Version.git_version
end
def check_namespace!(*objects)
......
module Gitlab
module Git
module Version
extend Gitlab::Git::Popen
def self.git_version
Gitlab::VersionInfo.parse(popen(%W(#{Gitlab.config.git.bin_path} --version), nil).first)
end
end
end
end
......@@ -213,25 +213,20 @@ module Gitlab
end
def create_from_bundle(bundle_path)
request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo)
enum = Enumerator.new do |y|
File.open(bundle_path, 'rb') do |f|
while data = f.read(MAX_MSG_SIZE)
request.data = data
y.yield request
request = Gitaly::CreateRepositoryFromBundleRequest.new
end
end
end
GitalyClient.call(
@storage,
:repository_service,
gitaly_repo_stream_request(
bundle_path,
:create_repository_from_bundle,
enum,
timeout: GitalyClient.default_timeout
Gitaly::CreateRepositoryFromBundleRequest,
GitalyClient.default_timeout
)
end
def restore_custom_hooks(custom_hooks_path)
gitaly_repo_stream_request(
custom_hooks_path,
:restore_custom_hooks,
Gitaly::RestoreCustomHooksRequest,
GitalyClient.default_timeout
)
end
......@@ -311,6 +306,30 @@ module Gitlab
request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
end
private
def gitaly_repo_stream_request(file_path, rpc_name, request_class, timeout)
request = request_class.new(repository: @gitaly_repo)
enum = Enumerator.new do |y|
File.open(file_path, 'rb') do |f|
while data = f.read(MAX_MSG_SIZE)
request.data = data
y.yield request
request = request_class.new
end
end
end
GitalyClient.call(
@storage,
:repository_service,
rpc_name,
enum,
timeout: timeout
)
end
end
end
end
......@@ -7,7 +7,7 @@ module QA
class Repository
include Scenario::Actable
attr_reader :push_error
attr_reader :push_output
def self.perform(*args)
Dir.mktmpdir do |dir|
......@@ -35,7 +35,7 @@ module QA
end
def clone(opts = '')
`git clone #{opts} #{@uri.to_s} ./ #{suppress_output}`
run_and_redact_credentials("git clone #{opts} #{@uri} ./")
end
def checkout(branch_name)
......@@ -71,8 +71,7 @@ module QA
end
def push_changes(branch = 'master')
# capture3 returns stdout, stderr and status.
_, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
@push_output, _ = run_and_redact_credentials("git push #{@uri} #{branch}")
end
def commits
......@@ -81,12 +80,10 @@ module QA
private
def suppress_output
# If we're running as the default user, it's probably a temporary
# instance and output can be useful for debugging
return if @username == Runtime::User.default_name
"&> #{File::NULL}"
# Since the remote URL contains the credentials, and git occasionally
# outputs the URL. Note that stderr is redirected to stdout.
def run_and_redact_credentials(command)
Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'")
end
end
end
......
......@@ -60,9 +60,9 @@ module QA
push_changes('protected-branch')
end
expect(repository.push_error)
expect(repository.push_output)
.to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
expect(repository.push_error)
expect(repository.push_output)
.to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
......
describe QA::Git::Repository do
let(:repository) { described_class.new }
before do
cd_empty_temp_directory
set_bad_uri
repository.use_default_credentials
end
describe '#clone' do
it 'redacts credentials from the URI in output' do
output, _ = repository.clone
expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'")
end
end
describe '#push_changes' do
before do
`git init` # need a repo to push from
end
it 'redacts credentials from the URI in output' do
output, _ = repository.push_changes
expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'")
end
end
def cd_empty_temp_directory
tmp_dir = 'tmp/git-repository-spec/'
FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir)
FileUtils.mkdir_p tmp_dir
FileUtils.cd tmp_dir
end
def set_bad_uri
repository.uri = 'http://foo/bar.git'
end
end
......@@ -28,7 +28,7 @@ feature 'Admin uses repository checks' do
visit_admin_project_page(project)
page.within('.alert') do
expect(page.text).to match(/Last repository check \(.* ago\) failed/)
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
......
......@@ -139,7 +139,7 @@ describe 'Merge request > User posts notes', :js do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text)
.to match(/less than a minute ago/)
.to match(/just now/)
end
end
end
......
......@@ -50,7 +50,7 @@ describe('DetailsSectionMixin', () => {
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStaleMessage).toBe('Data is out of date from about a minute ago');
expect(vm.statusInfoStaleMessage).toBe('Data is out of date from 1 minute ago');
});
});
});
......
......@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
......@@ -25,13 +24,10 @@ describe('RepoEditor', () => {
f.tempFile = true;
vm.$store.state.openFiles.push(f);
Vue.set(vm.$store.state.entries, f.path, f);
vm.monaco = true;
vm.$mount();
monacoLoader(['vs/editor/editor.main'], () => {
setTimeout(done, 0);
});
Vue.nextTick(() => setTimeout(done));
});
afterEach(() => {
......
/* global monaco */
import eventHub from '~/ide/eventhub';
import monacoLoader from '~/ide/monaco_loader';
import ModelManager from '~/ide/lib/common/model_manager';
import { file } from '../../helpers';
describe('Multi-file editor library model manager', () => {
let instance;
beforeEach(done => {
monacoLoader(['vs/editor/editor.main'], () => {
instance = new ModelManager(monaco);
done();
});
beforeEach(() => {
instance = new ModelManager();
});
afterEach(() => {
......
/* global monaco */
import eventHub from '~/ide/eventhub';
import monacoLoader from '~/ide/monaco_loader';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library model', () => {
let model;
beforeEach(done => {
beforeEach(() => {
spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => {
const f = file('path');
f.mrChange = { diff: 'ABC' };
f.baseRaw = 'test';
model = new Model(monaco, f);
done();
});
const f = file('path');
f.mrChange = { diff: 'ABC' };
f.baseRaw = 'test';
model = new Model(f);
});
afterEach(() => {
......@@ -38,7 +32,7 @@ describe('Multi-file editor library model', () => {
const f = file('path');
model.dispose();
model = new Model(monaco, f, {
model = new Model(f, {
...f,
content: '123 testing',
});
......
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import Editor from '~/ide/lib/editor';
import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
......@@ -10,16 +8,12 @@ describe('Multi-file editor library decorations controller', () => {
let controller;
let model;
beforeEach(done => {
monacoLoader(['vs/editor/editor.main'], () => {
editorInstance = editor.create(monaco);
editorInstance.createInstance(document.createElement('div'));
beforeEach(() => {
editorInstance = Editor.create();
editorInstance.createInstance(document.createElement('div'));
controller = new DecorationsController(editorInstance);
model = new Model(monaco, file('path'));
done();
});
controller = new DecorationsController(editorInstance);
model = new Model(file('path'));
});
afterEach(() => {
......
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import { Range } from 'monaco-editor';
import Editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
......@@ -14,20 +13,16 @@ describe('Multi-file editor library dirty diff controller', () => {
let decorationsController;
let model;
beforeEach(done => {
monacoLoader(['vs/editor/editor.main'], () => {
editorInstance = editor.create(monaco);
editorInstance.createInstance(document.createElement('div'));
beforeEach(() => {
editorInstance = Editor.create();
editorInstance.createInstance(document.createElement('div'));
modelManager = new ModelManager(monaco);
decorationsController = new DecorationsController(editorInstance);
modelManager = new ModelManager();
decorationsController = new DecorationsController(editorInstance);
model = modelManager.addModel(file('path'));
model = modelManager.addModel(file('path'));
controller = new DirtyDiffController(modelManager, decorationsController);
done();
});
controller = new DirtyDiffController(modelManager, decorationsController);
});
afterEach(() => {
......@@ -170,7 +165,7 @@ describe('Multi-file editor library dirty diff controller', () => {
[],
[
{
range: new monaco.Range(1, 1, 1, 1),
range: new Range(1, 1, 1, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
......
/* global monaco */
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import { editor as monacoEditor } from 'monaco-editor';
import Editor from '~/ide/lib/editor';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
......@@ -8,18 +7,14 @@ describe('Multi-file editor library', () => {
let el;
let holder;
beforeEach(done => {
beforeEach(() => {
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
monacoLoader(['vs/editor/editor.main'], () => {
instance = editor.create(monaco);
done();
});
instance = Editor.create();
});
afterEach(() => {
......@@ -29,20 +24,20 @@ describe('Multi-file editor library', () => {
});
it('creates instance of editor', () => {
expect(editor.editorInstance).not.toBeNull();
expect(Editor.editorInstance).not.toBeNull();
});
it('creates instance returns cached instance', () => {
expect(editor.create(monaco)).toEqual(instance);
expect(Editor.create()).toEqual(instance);
});
describe('createInstance', () => {
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough();
spyOn(monacoEditor, 'create').and.callThrough();
instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled();
expect(monacoEditor.create).toHaveBeenCalled();
});
it('creates dirty diff controller', () => {
......@@ -60,11 +55,11 @@ describe('Multi-file editor library', () => {
describe('createDiffInstance', () => {
it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
spyOn(monacoEditor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder);
expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, {
expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
model: null,
contextmenu: true,
minimap: {
......
import monacoContext from 'monaco-editor/dev/vs/loader';
import monacoLoader from '~/ide/monaco_loader';
describe('MonacoLoader', () => {
it('calls require.config and exports require', () => {
expect(monacoContext.require.getConfig()).toEqual(
jasmine.objectContaining({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
}),
);
expect(monacoLoader).toBe(monacoContext.require);
});
});
......@@ -68,6 +68,30 @@ describe Group do
end
end
describe '#notification_settings', :nested_groups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:sub_group) { create(:group, parent_id: group.id) }
before do
group.add_developer(user)
sub_group.add_developer(user)
end
it 'also gets notification settings from parent groups' do
expect(sub_group.notification_settings.size).to eq(2)
expect(sub_group.notification_settings).to include(group.notification_settings.first)
end
context 'when sub group is deleted' do
it 'does not delete parent notification settings' do
expect do
sub_group.destroy
end.to change { NotificationSetting.count }.by(-1)
end
end
end
describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) }
......
......@@ -13,4 +13,48 @@ describe NotificationRecipient do
expect(recipient.has_access?).to be_falsy
end
context '#notification_setting' do
context 'for child groups', :nested_groups do
let!(:moved_group) { create(:group) }
let(:group) { create(:group) }
let(:sub_group_1) { create(:group, parent: group) }
let(:sub_group_2) { create(:group, parent: sub_group_1) }
let(:project) { create(:project, namespace: moved_group) }
before do
sub_group_2.add_owner(user)
moved_group.add_owner(user)
Groups::TransferService.new(moved_group, user).execute(sub_group_2)
moved_group.reload
end
context 'when notification setting is global' do
before do
user.notification_settings_for(group).global!
user.notification_settings_for(sub_group_1).mention!
user.notification_settings_for(sub_group_2).global!
user.notification_settings_for(moved_group).global!
end
it 'considers notification setting from the first parent without global setting' do
expect(subject.notification_setting.source).to eq(sub_group_1)
end
end
context 'when notification setting is not global' do
before do
user.notification_settings_for(group).global!
user.notification_settings_for(sub_group_1).mention!
user.notification_settings_for(sub_group_2).watch!
user.notification_settings_for(moved_group).disabled!
end
it 'considers notification setting from lowest group member in hierarchy' do
expect(subject.notification_setting.source).to eq(moved_group)
end
end
end
end
end
This diff is collapsed.
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::HousekeepingService do
subject { described_class.new(project) }
let(:project) { create(:project, :repository) }
set(:project) { create(:project, :repository) }
before do
project.reset_pushes_since_gc
......@@ -16,12 +16,12 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
expect(subject).to receive(:lease_key).and_return(:the_lease_key)
expect(subject).to receive(:task).and_return(:the_task)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid)
expect(subject).to receive(:task).and_return(:incremental_repack)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
subject.execute
expect(project.reload.pushes_since_gc).to eq(0)
Sidekiq::Testing.fake! do
expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
end
end
it 'yields the block if given' do
......@@ -30,6 +30,16 @@ describe Projects::HousekeepingService do
end.to yield_with_no_args
end
it 'resets counter after execution' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
allow(subject).to receive(:gc_period).and_return(1)
project.increment_pushes_since_gc
Sidekiq::Testing.inline! do
expect { subject.execute }.to change { project.pushes_since_gc }.to(0)
end
end
context 'when no lease can be obtained' do
before do
expect(subject).to receive(:try_obtain_lease).and_return(false)
......@@ -54,6 +64,30 @@ describe Projects::HousekeepingService do
end.not_to yield_with_no_args
end
end
context 'task type' do
it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do
allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
# At push 200
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid)
.exactly(1).times
# At push 50, 100, 150
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid)
.exactly(3).times
# At push 10, 20, ... (except those above)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
201.times do
subject.increment!
subject.execute if subject.needed?
end
expect(project.pushes_since_gc).to eq(1)
end
end
end
describe '#needed?' do
......@@ -69,31 +103,7 @@ describe Projects::HousekeepingService do
describe '#increment!' do
it 'increments the pushes_since_gc counter' do
expect do
subject.increment!
end.to change { project.pushes_since_gc }.from(0).to(1)
expect { subject.increment! }.to change { project.pushes_since_gc }.by(1)
end
end
it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do
allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
# At push 200
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid)
.exactly(1).times
# At push 50, 100, 150
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid)
.exactly(3).times
# At push 10, 20, ... (except those above)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
201.times do
subject.increment!
subject.execute if subject.needed?
end
expect(project.pushes_since_gc).to eq(1)
end
end
require 'spec_helper'
shared_examples 'reportable note' do |type|
include MobileHelpers
include NotesHelper
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
......@@ -39,6 +40,9 @@ shared_examples 'reportable note' do |type|
end
def open_dropdown(dropdown)
# make window wide enough that tooltip doesn't trigger horizontal scrollbar
resize_window(1200, 800)
dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
end
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment