Commit 9b8dc8c6 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-06-08

# Conflicts:
#	app/assets/javascripts/boards/models/list.js
#	app/assets/javascripts/boards/stores/boards_store.js
#	app/finders/issues_finder.rb
#	app/services/boards/lists/create_service.rb
#	app/views/projects/settings/ci_cd/_autodevops_form.html.haml
#	app/views/shared/issuable/_search_bar.html.haml

[ci skip]
parents 85a62776 f068479e
...@@ -434,7 +434,7 @@ group :ed25519 do ...@@ -434,7 +434,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.101.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -307,7 +307,7 @@ GEM ...@@ -307,7 +307,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.100.0) gitaly-proto (0.101.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1074,7 +1074,7 @@ DEPENDENCIES ...@@ -1074,7 +1074,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.100.0) gitaly-proto (~> 0.101.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
......
...@@ -142,4 +142,3 @@ export default { ...@@ -142,4 +142,3 @@ export default {
</div> </div>
</div> </div>
</template> </template>
...@@ -55,8 +55,12 @@ class List { ...@@ -55,8 +55,12 @@ class List {
entityType = 'assignee_id'; entityType = 'assignee_id';
} }
<<<<<<< HEAD
return gl.boardService return gl.boardService
.createList(entity.id, entityType) .createList(entity.id, entityType)
=======
return gl.boardService.createList(this.label.id)
>>>>>>> upstream/master
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
this.id = data.id; this.id = data.id;
......
...@@ -161,6 +161,7 @@ gl.issueBoards.BoardsStore = { ...@@ -161,6 +161,7 @@ gl.issueBoards.BoardsStore = {
return list[key] === val && byType; return list[key] === val && byType;
}); });
return filteredList[0]; return filteredList[0];
<<<<<<< HEAD
}, },
updateFiltersUrl (replaceState = false) { updateFiltersUrl (replaceState = false) {
if (replaceState) { if (replaceState) {
...@@ -168,6 +169,8 @@ gl.issueBoards.BoardsStore = { ...@@ -168,6 +169,8 @@ gl.issueBoards.BoardsStore = {
} else { } else {
history.pushState(null, null, `?${this.filter.path}`); history.pushState(null, null, `?${this.filter.path}`);
} }
=======
>>>>>>> upstream/master
}, },
}; };
......
...@@ -69,9 +69,10 @@ export default () => { ...@@ -69,9 +69,10 @@ export default () => {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
if (!hasVueMRDiscussionsCookie()) { const resolveCountAppEl = document.querySelector('#resolve-count-app');
if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) {
new Vue({ new Vue({
el: '#resolve-count-app', el: resolveCountAppEl,
components: { components: {
'resolve-count': ResolveCount 'resolve-count': ResolveCount
}, },
......
...@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants'; import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue'; import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue'; import JobsDetail from '../jobs/detail.vue';
import ResizablePanel from '../resizable_panel.vue';
export default { export default {
directives: { directives: {
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
Icon, Icon,
PipelinesList, PipelinesList,
JobsDetail, JobsDetail,
ResizablePanel,
}, },
computed: { computed: {
...mapState(['rightPane']), ...mapState(['rightPane']),
...@@ -40,12 +42,16 @@ export default { ...@@ -40,12 +42,16 @@ export default {
<div <div
class="multi-file-commit-panel ide-right-sidebar" class="multi-file-commit-panel ide-right-sidebar"
> >
<div <resizable-panel
class="multi-file-commit-panel-inner"
v-if="rightPane" v-if="rightPane"
class="multi-file-commit-panel-inner"
:collapsible="false"
:initial-width="350"
:min-size="350"
side="right"
> >
<component :is="rightPane" /> <component :is="rightPane" />
</div> </resizable-panel>
<nav class="ide-activity-bar"> <nav class="ide-activity-bar">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li> <li>
......
<script> <script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
...@@ -50,7 +48,7 @@ export default { ...@@ -50,7 +48,7 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently // Compare key to allow for files opened in review mode to be cached differently
if (oldVal.key !== this.file.key) { if (oldVal.key !== this.file.key) {
this.initMonaco(); this.initEditor();
if (this.currentActivityView !== activityBarViews.edit) { if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({ this.setFileViewMode({
...@@ -84,15 +82,10 @@ export default { ...@@ -84,15 +82,10 @@ export default {
this.editor.dispose(); this.editor.dispose();
}, },
mounted() { mounted() {
if (this.editor && monaco) { if (!this.editor) {
this.initMonaco(); this.editor = Editor.create();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.editor = Editor.create(monaco);
this.initMonaco();
});
} }
this.initEditor();
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -105,7 +98,7 @@ export default { ...@@ -105,7 +98,7 @@ export default {
'updateViewer', 'updateViewer',
'removePendingTab', 'removePendingTab',
]), ]),
initMonaco() { initEditor() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
this.editor.clearEditor(); this.editor.clearEditor();
...@@ -118,7 +111,7 @@ export default { ...@@ -118,7 +111,7 @@ export default {
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch(err => { .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; throw err;
}); });
}, },
......
import { editor as monacoEditor, Uri } from 'monaco-editor';
import Disposable from './disposable'; import Disposable from './disposable';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
export default class Model { export default class Model {
constructor(monaco, file, head = null) { constructor(file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable(); this.disposable = new Disposable();
this.file = file; this.file = file;
this.head = head; this.head = head;
this.content = file.content !== '' ? file.content : file.raw; this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add( this.disposable.add(
(this.originalModel = this.monaco.editor.createModel( (this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw, head ? head.content : this.file.raw,
undefined, 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, this.content,
undefined, undefined,
new this.monaco.Uri(null, null, this.path), new Uri(false, false, this.path),
)), )),
); );
if (this.file.mrChange) { if (this.file.mrChange) {
this.disposable.add( this.disposable.add(
(this.baseModel = this.monaco.editor.createModel( (this.baseModel = monacoEditor.createModel(
this.file.baseRaw, this.file.baseRaw,
undefined, 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'; ...@@ -3,8 +3,7 @@ import Disposable from './disposable';
import Model from './model'; import Model from './model';
export default class ModelManager { export default class ModelManager {
constructor(monaco) { constructor() {
this.monaco = monaco;
this.disposable = new Disposable(); this.disposable = new Disposable();
this.models = new Map(); this.models = new Map();
} }
...@@ -22,7 +21,7 @@ export default class ModelManager { ...@@ -22,7 +21,7 @@ export default class ModelManager {
return this.getModel(file.key); 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.models.set(model.path, model);
this.disposable.add(model); this.disposable.add(model);
......
/* global monaco */ import { Range } from 'monaco-editor';
import { throttle } from 'underscore'; import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker'; import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable'; import Disposable from '../common/disposable';
...@@ -16,7 +16,7 @@ export const getDiffChangeType = change => { ...@@ -16,7 +16,7 @@ export const getDiffChangeType = change => {
}; };
export const getDecorator = 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: { options: {
isWholeLine: true, isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
......
import _ from 'underscore'; import _ from 'underscore';
import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores'; import store from '../stores';
import DecorationsController from './decorations/controller'; import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
...@@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options'; ...@@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme'; import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json'; import keymap from './keymap.json';
function setupMonacoTheme() {
monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
monacoEditor.setTheme('gitlab');
}
export const clearDomElement = el => { export const clearDomElement = el => {
if (!el || !el.firstChild) return; if (!el || !el.firstChild) return;
...@@ -17,24 +23,22 @@ export const clearDomElement = el => { ...@@ -17,24 +23,22 @@ export const clearDomElement = el => {
}; };
export default class Editor { export default class Editor {
static create(monaco) { static create() {
if (this.editorInstance) return this.editorInstance; if (!this.editorInstance) {
this.editorInstance = new Editor();
this.editorInstance = new Editor(monaco); }
return this.editorInstance; return this.editorInstance;
} }
constructor(monaco) { constructor() {
this.monaco = monaco;
this.currentModel = null; this.currentModel = null;
this.instance = null; this.instance = null;
this.dirtyDiffController = null; this.dirtyDiffController = null;
this.disposable = new Disposable(); this.disposable = new Disposable();
this.modelManager = new ModelManager(this.monaco); this.modelManager = new ModelManager();
this.decorationsController = new DecorationsController(this); this.decorationsController = new DecorationsController(this);
this.setupMonacoTheme(); setupMonacoTheme();
this.debouncedUpdate = _.debounce(() => { this.debouncedUpdate = _.debounce(() => {
this.updateDimensions(); this.updateDimensions();
...@@ -46,7 +50,7 @@ export default class Editor { ...@@ -46,7 +50,7 @@ export default class Editor {
clearDomElement(domElement); clearDomElement(domElement);
this.disposable.add( this.disposable.add(
(this.instance = this.monaco.editor.create(domElement, { (this.instance = monacoEditor.create(domElement, {
...defaultEditorOptions, ...defaultEditorOptions,
})), })),
(this.dirtyDiffController = new DirtyDiffController( (this.dirtyDiffController = new DirtyDiffController(
...@@ -66,7 +70,7 @@ export default class Editor { ...@@ -66,7 +70,7 @@ export default class Editor {
clearDomElement(domElement); clearDomElement(domElement);
this.disposable.add( this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, { (this.instance = monacoEditor.createDiffEditor(domElement, {
...defaultEditorOptions, ...defaultEditorOptions,
quickSuggestions: false, quickSuggestions: false,
occurrencesHighlight: false, occurrencesHighlight: false,
...@@ -122,17 +126,11 @@ export default class Editor { ...@@ -122,17 +126,11 @@ export default class Editor {
modified: model.getModel(), modified: model.getModel(),
}); });
this.monaco.editor.createDiffNavigator(this.instance, { monacoEditor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true, alwaysRevealFirst: true,
}); });
} }
setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
clearEditor() { clearEditor() {
if (this.instance) { if (this.instance) {
this.instance.setModel(null); this.instance.setModel(null);
...@@ -200,7 +198,7 @@ export default class Editor { ...@@ -200,7 +198,7 @@ export default class Editor {
const getKeyCode = key => { const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0; 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 => { 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() { ...@@ -79,37 +79,37 @@ export function getTimeago() {
if (!timeagoInstance) { if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) { const localeRemaining = function getLocaleRemaining(number, index) {
return [ return [
[s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
[s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
[s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
[s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|%s days ago'), s__('Timeago|%s days 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|%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|%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')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index]; ][index];
}; };
const locale = function getLocale(number, index) { const locale = function getLocale(number, index) {
return [ return [
[s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
[s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
[s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
[s__('Timeago|a day ago'), s__('Timeago|in 1 day')], [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|%s days ago'), s__('Timeago|in %s days')], [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|%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|%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')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index]; ][index];
}; };
......
...@@ -147,6 +147,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -147,6 +147,7 @@ document.addEventListener('DOMContentLoaded', () => {
$body.tooltip({ $body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]', selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover', trigger: 'hover',
boundary: 'viewport',
placement(tip, el) { placement(tip, el) {
return $(el).data('placement') || 'bottom'; return $(el).data('placement') || 'bottom';
}, },
......
...@@ -107,7 +107,7 @@ code { ...@@ -107,7 +107,7 @@ code {
background-color: $red-100; background-color: $red-100;
border-radius: 3px; border-radius: 3px;
.code & { .code > & {
background-color: inherit; background-color: inherit;
padding: unset; padding: unset;
} }
...@@ -233,6 +233,13 @@ table { ...@@ -233,6 +233,13 @@ table {
} }
} }
.card-header {
h3.card-title,
h4.card-title {
margin-top: 0;
}
}
.nav-tabs { .nav-tabs {
// Override bootstrap's default border // Override bootstrap's default border
border-bottom: 0; border-bottom: 0;
......
...@@ -497,6 +497,10 @@ fieldset[disabled] .btn, ...@@ -497,6 +497,10 @@ fieldset[disabled] .btn,
} }
} }
[readonly] {
cursor: default;
}
.btn-no-padding { .btn-no-padding {
padding: 0; padding: 0;
} }
...@@ -174,11 +174,6 @@ $border-gray-normal: darken($gray-normal, $darken-border-factor); ...@@ -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-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor); $border-gray-dark: darken($white-normal, $darken-border-factor);
/*
* Override Bootstrap 4 variables
*/
$secondary: $gray-light;
/* /*
* UI elements * UI elements
*/ */
...@@ -846,3 +841,14 @@ Prometheus ...@@ -846,3 +841,14 @@ Prometheus
$prometheus-table-row-highlight-color: $theme-gray-100; $prometheus-table-row-highlight-color: $theme-gray-100;
$priority-label-empty-state-width: 114px; $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 @@ ...@@ -18,7 +18,8 @@
.file-finder-input:hover, .file-finder-input:hover,
.issuable-search-form:hover, .issuable-search-form:hover,
.search-text-input:hover, .search-text-input:hover,
.form-control:hover { .form-control:hover,
:not[readonly] {
border-color: lighten($dropdown-input-focus-border, 20%); border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
} }
......
...@@ -144,6 +144,7 @@ class IssuesFinder < IssuableFinder ...@@ -144,6 +144,7 @@ class IssuesFinder < IssuableFinder
items items
end end
end end
<<<<<<< HEAD
def assignees def assignees
return @assignees if defined?(@assignees) return @assignees if defined?(@assignees)
...@@ -157,4 +158,6 @@ class IssuesFinder < IssuableFinder ...@@ -157,4 +158,6 @@ class IssuesFinder < IssuableFinder
[] []
end end
end end
=======
>>>>>>> upstream/master
end end
...@@ -60,7 +60,7 @@ module IconsHelper ...@@ -60,7 +60,7 @@ module IconsHelper
def spinner(text = nil, visible = false) def spinner(text = nil, visible = false)
css_class = 'loading' css_class = 'loading'
css_class << ' hidden' unless visible css_class << ' hide' unless visible
content_tag :div, class: css_class do content_tag :div, class: css_class do
icon('spinner spin') + text icon('spinner spin') + text
......
...@@ -167,7 +167,7 @@ module IssuablesHelper ...@@ -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_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 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 output.html_safe
end end
......
...@@ -13,6 +13,7 @@ class Group < Namespace ...@@ -13,6 +13,7 @@ class Group < Namespace
include GroupDescendant include GroupDescendant
include TokenAuthenticatable include TokenAuthenticatable
include WithUploads include WithUploads
include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members alias_method :members, :group_members
...@@ -28,7 +29,11 @@ class Group < Namespace ...@@ -28,7 +29,11 @@ class Group < Namespace
has_many :milestones has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project 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 :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel' has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable' has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute' has_many :custom_attributes, class_name: 'GroupCustomAttribute'
...@@ -90,6 +95,15 @@ class Group < Namespace ...@@ -90,6 +95,15 @@ class Group < Namespace
end end
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) def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}" "#{self.class.reference_prefix}#{full_path}"
end end
...@@ -227,6 +241,12 @@ class Group < Namespace ...@@ -227,6 +241,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id) members_with_parents.pluck(:user_id)
end end
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
end
end
def members_with_parents def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents # Avoids an unnecessary SELECT when the group has no parents
source_ids = source_ids =
......
class NotificationRecipient class NotificationRecipient
include Gitlab::Utils::StrongMemoize
attr_reader :user, :type, :reason attr_reader :user, :type, :reason
def initialize(user, type, **opts) def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription unless NotificationSetting.levels.key?(type) || type == :subscription
...@@ -142,10 +144,33 @@ class NotificationRecipient ...@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global? 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 user.global_notification_setting
end 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 end
module Boards module Boards
module Lists module Lists
class CreateService < Boards::BaseService class CreateService < Boards::BaseService
<<<<<<< HEAD
prepend EE::Boards::Lists::CreateService prepend EE::Boards::Lists::CreateService
=======
>>>>>>> upstream/master
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def execute(board) def execute(board)
......
...@@ -7,5 +7,4 @@ ...@@ -7,5 +7,4 @@
= render 'shared/service_settings', form: form, subject: @service = render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block .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 @@ ...@@ -3,5 +3,5 @@
Groups with access to Groups with access to
%strong= @project.name %strong= @project.name
%span.badge.badge-pill= group_links.size %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 = render partial: 'shared/members/group', collection: group_links, as: :group_link
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list .protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty? - if @protected_branches.empty?
.card-header .card-header.bg-white
%h3.card-title %h3.card-title.mb-0
Protected branch (#{@protected_branches_count}) Protected branch (#{@protected_branches_count})
%p.settings-message.text-center %p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above. There are currently no protected branches, protect a branch with the form above.
......
- enabled = Gitlab.config.mattermost.enabled - enabled = Gitlab.config.mattermost.enabled
.card .info-well
%p .well-segment
This service allows users to perform common operations on this %p
project by entering slash commands in Mattermost. This service allows users to perform common operations on this
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do project by entering slash commands in Mattermost.
View documentation = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
= icon('external-link') View documentation
%p.inline = icon('external-link')
See list of available commands in Mattermost after setting up this service, %p.inline
by entering See list of available commands in Mattermost after setting up this service,
%kbd.inline /&lt;trigger&gt; help by entering
- unless enabled || @service.template? %kbd.inline /&lt;trigger&gt; help
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.template? - if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.card .info-well
%p .well-segment
This service allows users to perform common operations on this %p
project by entering slash commands in Slack. This service allows users to perform common operations on this
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do project by entering slash commands in Slack.
View documentation = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
= icon('external-link') View documentation
%p.inline = icon('external-link')
See list of available commands in Slack after setting up this service, %p.inline
by entering See list of available commands in Slack after setting up this service,
%kbd.inline /&lt;command&gt; help by entering
- unless @service.template? %kbd.inline /&lt;command&gt; help
%p To setup this service: - unless @service.template?
%ul.list-unstyled.indent-list %p To setup this service:
%li %ul.list-unstyled.indent-list
1. %li
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do 1.
Add a slash command = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
= icon('external-link') Add a slash command
in your Slack team with these options: = icon('external-link')
in your Slack team with these options:
%hr %hr
.help-form .help-form
.form-group .form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block .col-sm-10.col-12.text-block
%p Fill in the word that works best for your team. %p Fill in the word that works best for your team.
%p %p
Suggestions: Suggestions:
%code= 'gitlab' %code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes %code= @project.path # Path contains no spaces, but dashes
%code= @project.full_path %code= @project.full_path
.form-group .form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group .col-sm-10.col-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#url', class: 'input-group-text') = clipboard_button(target: '#url', class: 'input-group-text')
.form-group .form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block POST .col-sm-10.col-12.text-block POST
.form-group .form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group .col-sm-10.col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text') = clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group .form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.text-block .col-sm-10.col-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) = 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') = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group .form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' = 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 .col-sm-10.col-12.text-block Show this command in the autocomplete list
.form-group .form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group .col-sm-10.col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text') = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group .form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group .col-sm-10.col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group .form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
.col-sm-10.col-12.input-group .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' = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append .input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text') = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
%hr %hr
%ul.list-unstyled.indent-list %ul.list-unstyled.indent-list
%li %li
2. Paste the 2. Paste the
%strong Token %strong Token
into the field below into the field below
%li %li
3. Select the 3. Select the
%strong Active %strong Active
checkbox, press checkbox, press
%strong Save changes %strong Save changes
and start using GitLab inside Slack! and start using GitLab inside Slack!
...@@ -39,12 +39,15 @@ ...@@ -39,12 +39,15 @@
= s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
<<<<<<< HEAD
-# EE-specific start -# EE-specific start
.form-text.text-muted .form-text.text-muted
= s_('CICD|Do not set up a domain here if you are setting up multiple Kubernetes clusters with Auto DevOps.') = s_('CICD|Do not set up a domain here if you are setting up multiple Kubernetes clusters with Auto DevOps.')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters'), target: '_blank' = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters'), target: '_blank'
-# EE-specific end -# EE-specific end
=======
>>>>>>> upstream/master
%label.prepend-top-10 %label.prepend-top-10
%strong= s_('CICD|Deployment strategy') %strong= s_('CICD|Deployment strategy')
%p.settings-message.text-center %p.settings-message.text-center
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder .form-control.js-secret-value-placeholder
= '*' * 20 = '*' * 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 %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' } } %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value') = _('Reveal value')
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject = render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present? - elsif @service.help.present?
.card .info-well
= markdown @service.help .well-segment
= markdown @service.help
.service-settings .service-settings
- if @service.show_active_box? - if @service.show_active_box?
...@@ -15,25 +16,24 @@ ...@@ -15,25 +16,24 @@
- if @service.configurable_events.present? - if @service.configurable_events.present?
.form-group.row .form-group.row
= form.label :url, "Trigger", class: 'col-form-label col-sm-2' .col-sm-2.text-right Trigger
.col-sm-10 .col-sm-10
- @service.configurable_events.each do |event| - @service.configurable_events.each do |event|
%div .form-group
= form.check_box service_event_field_name(event), class: 'float-left' .form-check
.prepend-left-20 = form.check_box service_event_field_name(event), class: 'form-check-input'
= form.label service_event_field_name(event), class: 'list-label' do = form.label service_event_field_name(event), class: 'form-check-label' do
%strong %strong
= event.humanize = event.humanize
- field = @service.event_field(event) - field = @service.event_field(event)
- if field - if field
%p
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.light %p.text-muted
= @service.class.event_description(event) = @service.class.event_description(event)
- @service.global_fields.each do |field| - @service.global_fields.each do |field|
- type = field[:type] - 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
...@@ -122,10 +122,14 @@ ...@@ -122,10 +122,14 @@
= icon('times') = icon('times')
.filter-dropdown-container .filter-dropdown-container
- if type == :boards - if type == :boards
<<<<<<< HEAD
- user_can_admin_list = can?(current_user, :admin_list, board.parent) - user_can_admin_list = can?(current_user, :admin_list, board.parent)
.js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } } .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
- if user_can_admin_list - if user_can_admin_list
=======
- if can?(current_user, :admin_list, board.parent)
>>>>>>> upstream/master
= render_if_exists 'shared/issuable/board_create_list_dropdown', board: board = render_if_exists 'shared/issuable/board_create_list_dropdown', board: board
- if @project - if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
......
...@@ -5,16 +5,16 @@ ...@@ -5,16 +5,16 @@
%li.member.group_member{ id: dom_id } %li.member.group_member{ id: dom_id }
%span.list-item-name %span.list-item-name
= group_icon(group, class: "avatar s40", alt: '') = group_icon(group, class: "avatar s40", alt: '')
%strong .user-info
= link_to group.full_name, group_path(group) = link_to group.full_name, group_path(group), class: 'member'
.cgray .cgray
Given access #{time_ago_with_tooltip(group_link.created_at)} Given access #{time_ago_with_tooltip(group_link.created_at)}
- if group_link.expires? - if group_link.expires?
· ·
%span{ class: ('text-warning' if group_link.expires_soon?) } %span{ class: ('text-warning' if group_link.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls .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 = hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5 .member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
......
...@@ -5,6 +5,6 @@ ...@@ -5,6 +5,6 @@
- scopes.each do |scope| - scopes.each do |scope|
%fieldset %fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" = 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]) %span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc] .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 @@ ...@@ -534,3 +534,9 @@
:why: https://github.com/squaremo/bitsyntax-js/blob/master/LICENSE-MIT :why: https://github.com/squaremo/bitsyntax-js/blob/master/LICENSE-MIT
:versions: [] :versions: []
:when: 2018-02-20 22:20:25.958123000 Z :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 @@ ...@@ -29,14 +29,14 @@
label: Pod average label: Pod average
unit: ms unit: ms
- title: "HTTP Error Rate" - title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec" y_label: "HTTP Errors"
required_metrics: required_metrics:
- nginx_upstream_responses_total - nginx_upstream_responses_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))' - 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: HTTP Errors label: 5xx Errors
unit: "errors / sec" unit: "%"
- group: Response metrics (HA Proxy) - group: Response metrics (HA Proxy)
priority: 10 priority: 10
metrics: metrics:
......
...@@ -4,8 +4,8 @@ const glob = require('glob'); ...@@ -4,8 +4,8 @@ const glob = require('glob');
const webpack = require('webpack'); const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin'); const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ROOT_PATH = path.resolve(__dirname, '..'); const ROOT_PATH = path.resolve(__dirname, '..');
...@@ -181,15 +181,7 @@ module.exports = { ...@@ -181,15 +181,7 @@ module.exports = {
name: '[name].[hash:8].[ext]', 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: { optimization: {
...@@ -239,6 +231,9 @@ module.exports = { ...@@ -239,6 +231,9 @@ module.exports = {
// enable vue-loader to use existing loader rules for other module types // enable vue-loader to use existing loader rules for other module types
new VueLoaderPlugin(), new VueLoaderPlugin(),
// automatically configure monaco editor web workers
new MonacoWebpackPlugin(),
// prevent pikaday from including moment.js // prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/), new webpack.IgnorePlugin(/moment/, /pikaday/),
...@@ -248,29 +243,6 @@ module.exports = { ...@@ -248,29 +243,6 @@ module.exports = {
jQuery: 'jquery', 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 // compression can require a lot of compute time and is disabled in CI
IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(), IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
......
...@@ -162,13 +162,13 @@ such as Trello, JIRA, etc. ...@@ -162,13 +162,13 @@ such as Trello, JIRA, etc.
## Webhooks ## 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 specific events like pushes, issues or merge requests. GitLab will send a
POST request with data to the webhook URL. POST request with data to the webhook URL.
## API ## API
Automate GitLab via [API](../api/README.html). Automate GitLab via [API](../api/README.md).
## Git and GitLab ## Git and GitLab
......
...@@ -35,7 +35,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline ...@@ -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). 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. 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.) [//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
[//]: # (They are needed for the Markdown text to render correctly.) [//]: # (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- ...@@ -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 -]. 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: Examples:
...@@ -229,7 +229,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji ...@@ -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. 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: 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 ...@@ -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. 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: Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
...@@ -407,7 +407,7 @@ Examples: ...@@ -407,7 +407,7 @@ Examples:
`HSL(540,70%,50%)` `HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)` `HSLA(540,70%,50%,0.7)`
Becomes: Become:
`#F00` `#F00`
`#F00A` `#F00A`
...@@ -484,14 +484,14 @@ Alt-H2 ...@@ -484,14 +484,14 @@ Alt-H2
All Markdown-rendered headers automatically get IDs, except in comments. 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: The IDs are generated from the content of the header according to the following rules:
1. All text is converted to lowercase 1. All text is converted to lowercase.
1. All non-word text (e.g., punctuation, HTML) is removed 1. All non-word text (e.g., punctuation, HTML) is removed.
1. All spaces are converted to hyphens 1. All spaces are converted to hyphens.
1. Two or more hyphens in a row are converted to one 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 1. If a header with the same ID has already been generated, a unique
incrementing number is appended, starting at 1. 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 ...@@ -519,6 +519,8 @@ Note that the Emoji processing happens before the header IDs are generated, so t
### Emphasis ### Emphasis
Examples:
```no-highlight ```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_. Emphasis, aka italics, with *asterisks* or _underscores_.
...@@ -529,6 +531,8 @@ Combined emphasis with **asterisks and _underscores_**. ...@@ -529,6 +531,8 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~ Strikethrough uses two tildes. ~~Scratch this.~~
``` ```
Become:
Emphasis, aka italics, with *asterisks* or _underscores_. Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__. Strong emphasis, aka bold, with **asterisks** or __underscores__.
...@@ -539,6 +543,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ...@@ -539,6 +543,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
### Lists ### Lists
Examples:
```no-highlight ```no-highlight
1. First ordered list item 1. First ordered list item
2. Another item 2. Another item
...@@ -552,6 +558,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ...@@ -552,6 +558,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
+ Or pluses + Or pluses
``` ```
Become:
1. First ordered list item 1. First ordered list item
2. Another item 2. Another item
* Unordered sub-list. * Unordered sub-list.
...@@ -566,6 +574,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ...@@ -566,6 +574,8 @@ Strikethrough uses two tildes. ~~Scratch this.~~
If a list item contains multiple paragraphs, If a list item contains multiple paragraphs,
each subsequent paragraph should be indented with four spaces. each subsequent paragraph should be indented with four spaces.
Example:
```no-highlight ```no-highlight
1. First ordered list item 1. First ordered list item
...@@ -573,6 +583,8 @@ each subsequent paragraph should be indented with four spaces. ...@@ -573,6 +583,8 @@ each subsequent paragraph should be indented with four spaces.
2. Another item 2. Another item
``` ```
Becomes:
1. First ordered list item 1. First ordered list item
Second paragraph of first item. Second paragraph of first item.
...@@ -581,6 +593,8 @@ each subsequent paragraph should be indented with four spaces. ...@@ -581,6 +593,8 @@ each subsequent paragraph should be indented with four spaces.
If the second paragraph isn't indented with four spaces, If the second paragraph isn't indented with four spaces,
the second list item will be incorrectly labeled as `1`. the second list item will be incorrectly labeled as `1`.
Example:
```no-highlight ```no-highlight
1. First ordered list item 1. First ordered list item
...@@ -588,6 +602,8 @@ the second list item will be incorrectly labeled as `1`. ...@@ -588,6 +602,8 @@ the second list item will be incorrectly labeled as `1`.
2. Another item 2. Another item
``` ```
Becomes:
1. First ordered list item 1. First ordered list item
Second paragraph of first 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 ...@@ -625,6 +641,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown
### Images ### Images
Examples:
Here's our logo (hover to see the title text): Here's our logo (hover to see the title text):
Inline-style: Inline-style:
...@@ -635,6 +653,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown ...@@ -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 [logo]: img/markdown_logo.png
Become:
Here's our logo: Here's our logo:
Inline-style: Inline-style:
...@@ -649,6 +669,8 @@ Reference-style: ...@@ -649,6 +669,8 @@ Reference-style:
### Blockquotes ### Blockquotes
Examples:
```no-highlight ```no-highlight
> Blockquotes are very handy in email to emulate reply text. > Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote. > This line is part of the same quote.
...@@ -658,6 +680,8 @@ Quote break. ...@@ -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. > 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. > Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote. > 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. ...@@ -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. 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 ```no-highlight
<dl> <dl>
<dt>Definition list</dt> <dt>Definition list</dt>
...@@ -681,6 +707,8 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd ...@@ -681,6 +707,8 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
</dl> </dl>
``` ```
Become:
<dl> <dl>
<dt>Definition list</dt> <dt>Definition list</dt>
<dd>Is something people use sometimes.</dd> <dd>Is something people use sometimes.</dd>
...@@ -715,6 +743,8 @@ These details will remain hidden until expanded. ...@@ -715,6 +743,8 @@ These details will remain hidden until expanded.
### Horizontal Rule ### Horizontal Rule
Examples:
``` ```
Three or more... Three or more...
...@@ -731,6 +761,8 @@ ___ ...@@ -731,6 +761,8 @@ ___
Underscores Underscores
``` ```
Become:
Three or more... Three or more...
--- ---
...@@ -751,6 +783,8 @@ My basic recommendation for learning how line breaks work is to experiment and d ...@@ -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: Here are some things to try out:
Examples:
``` ```
Here's a line for us to start with. 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. ...@@ -765,6 +799,8 @@ This line is *on its own line*, because the previous line ends with two spaces.
spaces. spaces.
``` ```
Become:
Here's a line for us to start with. 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*. This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
...@@ -781,6 +817,8 @@ spaces. ...@@ -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. 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 | | 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 ...@@ -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 | | cell 3 | cell 4 |
``` ```
Code above produces next output: Becomes:
| header 1 | header 2 | | header 1 | header 2 |
| -------- | -------- | | -------- | -------- |
...@@ -799,7 +837,9 @@ Code above produces next output: ...@@ -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. 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 | | 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 ...@@ -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 | | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
``` ```
Becomes:
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | | Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: | | :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | | 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 ...@@ -815,11 +857,15 @@ By including colons in the header row, you can align the text within that column
### Footnotes ### Footnotes
Example:
``` ```
You can add footnotes to your text as follows.[^2] You can add footnotes to your text as follows.[^2]
[^2]: This is my awesome footnote. [^2]: This is my awesome footnote.
``` ```
Becomes:
You can add footnotes to your text as follows.[^2] You can add footnotes to your text as follows.[^2]
## Wiki-specific Markdown ## Wiki-specific Markdown
......
...@@ -14,7 +14,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI ...@@ -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) | | 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}-.*"}) | | 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 ## Configuring NGINX ingress monitoring
......
...@@ -34,9 +34,14 @@ anything that is set at Global Settings. ...@@ -34,9 +34,14 @@ anything that is set at Global Settings.
![notification settings](img/notification_group_settings.png) ![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 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 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. 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. 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 ...@@ -112,18 +112,31 @@ module Backup
end end
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) 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| in_path(path_to_tars(project)) do |dir|
path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do gitaly_migrate(:restore_custom_hooks) do |is_enabled|
path_to_repo(project) if is_enabled
end local_restore_custom_hooks(project, dir)
cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) else
gitaly_restore_custom_hooks(project, dir)
output, status = Gitlab::Popen.popen(cmd) end
unless status.zero?
progress_warn(project, cmd.join(' '), output)
end end
end end
end end
......
...@@ -62,7 +62,7 @@ module Gitlab ...@@ -62,7 +62,7 @@ module Gitlab
end end
def version def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first) Gitlab::Git::Version.git_version
end end
def check_namespace!(*objects) 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 ...@@ -213,25 +213,20 @@ module Gitlab
end end
def create_from_bundle(bundle_path) def create_from_bundle(bundle_path)
request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo) gitaly_repo_stream_request(
enum = Enumerator.new do |y| bundle_path,
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,
:create_repository_from_bundle, :create_repository_from_bundle,
enum, Gitaly::CreateRepositoryFromBundleRequest,
timeout: GitalyClient.default_timeout 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 end
...@@ -311,6 +306,30 @@ module Gitlab ...@@ -311,6 +306,30 @@ module Gitlab
request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches) GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
end 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 end
end end
...@@ -7,7 +7,7 @@ module QA ...@@ -7,7 +7,7 @@ module QA
class Repository class Repository
include Scenario::Actable include Scenario::Actable
attr_reader :push_error attr_reader :push_output
def self.perform(*args) def self.perform(*args)
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
...@@ -35,7 +35,7 @@ module QA ...@@ -35,7 +35,7 @@ module QA
end end
def clone(opts = '') def clone(opts = '')
`git clone #{opts} #{@uri.to_s} ./ #{suppress_output}` run_and_redact_credentials("git clone #{opts} #{@uri} ./")
end end
def checkout(branch_name) def checkout(branch_name)
...@@ -71,8 +71,7 @@ module QA ...@@ -71,8 +71,7 @@ module QA
end end
def push_changes(branch = 'master') def push_changes(branch = 'master')
# capture3 returns stdout, stderr and status. @push_output, _ = run_and_redact_credentials("git push #{@uri} #{branch}")
_, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
end end
def commits def commits
...@@ -81,12 +80,10 @@ module QA ...@@ -81,12 +80,10 @@ module QA
private private
def suppress_output # Since the remote URL contains the credentials, and git occasionally
# If we're running as the default user, it's probably a temporary # outputs the URL. Note that stderr is redirected to stdout.
# instance and output can be useful for debugging def run_and_redact_credentials(command)
return if @username == Runtime::User.default_name Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'")
"&> #{File::NULL}"
end end
end end
end end
......
...@@ -60,9 +60,9 @@ module QA ...@@ -60,9 +60,9 @@ module QA
push_changes('protected-branch') push_changes('protected-branch')
end 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/) .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\)/) .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end end
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 ...@@ -28,7 +28,7 @@ feature 'Admin uses repository checks' do
visit_admin_project_page(project) visit_admin_project_page(project)
page.within('.alert') do 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
end end
......
...@@ -139,7 +139,7 @@ describe 'Merge request > User posts notes', :js do ...@@ -139,7 +139,7 @@ describe 'Merge request > User posts notes', :js do
page.within("#note_#{note.id}") do page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago') is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text) expect(find('.note_edited_ago').text)
.to match(/less than a minute ago/) .to match(/just now/)
end end
end end
end end
......
...@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue'; import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor'; import Editor from '~/ide/lib/editor';
import { activityBarViews } from '~/ide/constants'; import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
...@@ -25,13 +24,10 @@ describe('RepoEditor', () => { ...@@ -25,13 +24,10 @@ describe('RepoEditor', () => {
f.tempFile = true; f.tempFile = true;
vm.$store.state.openFiles.push(f); vm.$store.state.openFiles.push(f);
Vue.set(vm.$store.state.entries, f.path, f); Vue.set(vm.$store.state.entries, f.path, f);
vm.monaco = true;
vm.$mount(); vm.$mount();
monacoLoader(['vs/editor/editor.main'], () => { Vue.nextTick(() => setTimeout(done));
setTimeout(done, 0);
});
}); });
afterEach(() => { afterEach(() => {
......
/* global monaco */
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
import monacoLoader from '~/ide/monaco_loader';
import ModelManager from '~/ide/lib/common/model_manager'; import ModelManager from '~/ide/lib/common/model_manager';
import { file } from '../../helpers'; import { file } from '../../helpers';
describe('Multi-file editor library model manager', () => { describe('Multi-file editor library model manager', () => {
let instance; let instance;
beforeEach(done => { beforeEach(() => {
monacoLoader(['vs/editor/editor.main'], () => { instance = new ModelManager();
instance = new ModelManager(monaco);
done();
});
}); });
afterEach(() => { afterEach(() => {
......
/* global monaco */
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
import monacoLoader from '~/ide/monaco_loader';
import Model from '~/ide/lib/common/model'; import Model from '~/ide/lib/common/model';
import { file } from '../../helpers'; import { file } from '../../helpers';
describe('Multi-file editor library model', () => { describe('Multi-file editor library model', () => {
let model; let model;
beforeEach(done => { beforeEach(() => {
spyOn(eventHub, '$on').and.callThrough(); spyOn(eventHub, '$on').and.callThrough();
monacoLoader(['vs/editor/editor.main'], () => { const f = file('path');
const f = file('path'); f.mrChange = { diff: 'ABC' };
f.mrChange = { diff: 'ABC' }; f.baseRaw = 'test';
f.baseRaw = 'test'; model = new Model(f);
model = new Model(monaco, f);
done();
});
}); });
afterEach(() => { afterEach(() => {
...@@ -38,7 +32,7 @@ describe('Multi-file editor library model', () => { ...@@ -38,7 +32,7 @@ describe('Multi-file editor library model', () => {
const f = file('path'); const f = file('path');
model.dispose(); model.dispose();
model = new Model(monaco, f, { model = new Model(f, {
...f, ...f,
content: '123 testing', content: '123 testing',
}); });
......
/* global monaco */ import Editor from '~/ide/lib/editor';
import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import DecorationsController from '~/ide/lib/decorations/controller'; import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model'; import Model from '~/ide/lib/common/model';
import { file } from '../../helpers'; import { file } from '../../helpers';
...@@ -10,16 +8,12 @@ describe('Multi-file editor library decorations controller', () => { ...@@ -10,16 +8,12 @@ describe('Multi-file editor library decorations controller', () => {
let controller; let controller;
let model; let model;
beforeEach(done => { beforeEach(() => {
monacoLoader(['vs/editor/editor.main'], () => { editorInstance = Editor.create();
editorInstance = editor.create(monaco); editorInstance.createInstance(document.createElement('div'));
editorInstance.createInstance(document.createElement('div'));
controller = new DecorationsController(editorInstance); controller = new DecorationsController(editorInstance);
model = new Model(monaco, file('path')); model = new Model(file('path'));
done();
});
}); });
afterEach(() => { afterEach(() => {
......
/* global monaco */ import { Range } from 'monaco-editor';
import monacoLoader from '~/ide/monaco_loader'; import Editor from '~/ide/lib/editor';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager'; import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller'; import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
...@@ -14,20 +13,16 @@ describe('Multi-file editor library dirty diff controller', () => { ...@@ -14,20 +13,16 @@ describe('Multi-file editor library dirty diff controller', () => {
let decorationsController; let decorationsController;
let model; let model;
beforeEach(done => { beforeEach(() => {
monacoLoader(['vs/editor/editor.main'], () => { editorInstance = Editor.create();
editorInstance = editor.create(monaco); editorInstance.createInstance(document.createElement('div'));
editorInstance.createInstance(document.createElement('div'));
modelManager = new ModelManager(monaco); modelManager = new ModelManager();
decorationsController = new DecorationsController(editorInstance); decorationsController = new DecorationsController(editorInstance);
model = modelManager.addModel(file('path')); model = modelManager.addModel(file('path'));
controller = new DirtyDiffController(modelManager, decorationsController); controller = new DirtyDiffController(modelManager, decorationsController);
done();
});
}); });
afterEach(() => { afterEach(() => {
...@@ -170,7 +165,7 @@ describe('Multi-file editor library dirty diff controller', () => { ...@@ -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: { options: {
isWholeLine: true, isWholeLine: true,
linesDecorationsClassName: 'dirty-diff dirty-diff-modified', linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
......
/* global monaco */ import { editor as monacoEditor } from 'monaco-editor';
import monacoLoader from '~/ide/monaco_loader'; import Editor from '~/ide/lib/editor';
import editor from '~/ide/lib/editor';
import { file } from '../helpers'; import { file } from '../helpers';
describe('Multi-file editor library', () => { describe('Multi-file editor library', () => {
...@@ -8,18 +7,14 @@ describe('Multi-file editor library', () => { ...@@ -8,18 +7,14 @@ describe('Multi-file editor library', () => {
let el; let el;
let holder; let holder;
beforeEach(done => { beforeEach(() => {
el = document.createElement('div'); el = document.createElement('div');
holder = document.createElement('div'); holder = document.createElement('div');
el.appendChild(holder); el.appendChild(holder);
document.body.appendChild(el); document.body.appendChild(el);
monacoLoader(['vs/editor/editor.main'], () => { instance = Editor.create();
instance = editor.create(monaco);
done();
});
}); });
afterEach(() => { afterEach(() => {
...@@ -29,20 +24,20 @@ describe('Multi-file editor library', () => { ...@@ -29,20 +24,20 @@ describe('Multi-file editor library', () => {
}); });
it('creates instance of editor', () => { it('creates instance of editor', () => {
expect(editor.editorInstance).not.toBeNull(); expect(Editor.editorInstance).not.toBeNull();
}); });
it('creates instance returns cached instance', () => { it('creates instance returns cached instance', () => {
expect(editor.create(monaco)).toEqual(instance); expect(Editor.create()).toEqual(instance);
}); });
describe('createInstance', () => { describe('createInstance', () => {
it('creates editor instance', () => { it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'create').and.callThrough(); spyOn(monacoEditor, 'create').and.callThrough();
instance.createInstance(holder); instance.createInstance(holder);
expect(instance.monaco.editor.create).toHaveBeenCalled(); expect(monacoEditor.create).toHaveBeenCalled();
}); });
it('creates dirty diff controller', () => { it('creates dirty diff controller', () => {
...@@ -60,11 +55,11 @@ describe('Multi-file editor library', () => { ...@@ -60,11 +55,11 @@ describe('Multi-file editor library', () => {
describe('createDiffInstance', () => { describe('createDiffInstance', () => {
it('creates editor instance', () => { it('creates editor instance', () => {
spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough(); spyOn(monacoEditor, 'createDiffEditor').and.callThrough();
instance.createDiffInstance(holder); instance.createDiffInstance(holder);
expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, { expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
model: null, model: null,
contextmenu: true, contextmenu: true,
minimap: { 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 ...@@ -68,6 +68,30 @@ describe Group do
end end
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 describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) } let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) } let(:sub_group) { build(:group, parent_id: parent.id) }
......
...@@ -13,4 +13,48 @@ describe NotificationRecipient do ...@@ -13,4 +13,48 @@ describe NotificationRecipient do
expect(recipient.has_access?).to be_falsy expect(recipient.has_access?).to be_falsy
end 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 end
This diff is collapsed.
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::HousekeepingService do describe Projects::HousekeepingService do
subject { described_class.new(project) } subject { described_class.new(project) }
let(:project) { create(:project, :repository) } set(:project) { create(:project, :repository) }
before do before do
project.reset_pushes_since_gc project.reset_pushes_since_gc
...@@ -16,12 +16,12 @@ describe Projects::HousekeepingService do ...@@ -16,12 +16,12 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) 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(:lease_key).and_return(:the_lease_key)
expect(subject).to receive(:task).and_return(:the_task) expect(subject).to receive(:task).and_return(:incremental_repack)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid) expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
subject.execute Sidekiq::Testing.fake! do
expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
expect(project.reload.pushes_since_gc).to eq(0) end
end end
it 'yields the block if given' do it 'yields the block if given' do
...@@ -30,6 +30,16 @@ describe Projects::HousekeepingService do ...@@ -30,6 +30,16 @@ describe Projects::HousekeepingService do
end.to yield_with_no_args end.to yield_with_no_args
end 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 context 'when no lease can be obtained' do
before do before do
expect(subject).to receive(:try_obtain_lease).and_return(false) expect(subject).to receive(:try_obtain_lease).and_return(false)
...@@ -54,6 +64,30 @@ describe Projects::HousekeepingService do ...@@ -54,6 +64,30 @@ describe Projects::HousekeepingService do
end.not_to yield_with_no_args end.not_to yield_with_no_args
end end
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 end
describe '#needed?' do describe '#needed?' do
...@@ -69,31 +103,7 @@ describe Projects::HousekeepingService do ...@@ -69,31 +103,7 @@ describe Projects::HousekeepingService do
describe '#increment!' do describe '#increment!' do
it 'increments the pushes_since_gc counter' do it 'increments the pushes_since_gc counter' do
expect do expect { subject.increment! }.to change { project.pushes_since_gc }.by(1)
subject.increment!
end.to change { project.pushes_since_gc }.from(0).to(1)
end end
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 end
require 'spec_helper' require 'spec_helper'
shared_examples 'reportable note' do |type| shared_examples 'reportable note' do |type|
include MobileHelpers
include NotesHelper include NotesHelper
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
...@@ -39,6 +40,9 @@ shared_examples 'reportable note' do |type| ...@@ -39,6 +40,9 @@ shared_examples 'reportable note' do |type|
end end
def open_dropdown(dropdown) 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('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first) dropdown.find('.dropdown-menu li', match: :first)
end 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