Commit 03416324 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-09-07' into 'master'

CE upstream - 2018-09-07 16:58 UTC

Closes #6948, gitlab-ce#51225, gitlab-ce#40529, gitaly#954, and #7482

See merge request gitlab-org/gitlab-ee!7285
parents bef4c774 a3e92224
<script>
import $ from 'jquery';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
export default {
components: {
FileIcon,
ChangedFileIcon,
},
props: {
activeFile: {
type: Object,
required: true,
},
},
computed: {
activeButtonText() {
return this.activeFile.staged ? __('Unstage') : __('Stage');
},
isStaged() {
return !this.activeFile.changed && this.activeFile.staged;
},
},
methods: {
...mapActions(['stageChange', 'unstageChange']),
actionButtonClicked() {
if (this.activeFile.staged) {
this.unstageChange(this.activeFile.path);
} else {
this.stageChange(this.activeFile.path);
}
},
showDiscardModal() {
$(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
},
},
};
</script>
<template>
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon
:file-name="activeFile.name"
:size="16"
class="mr-2"
/>
<strong class="mr-2">
{{ activeFile.path }}
</strong>
<changed-file-icon
:file="activeFile"
/>
<div class="ml-auto">
<button
v-if="!isStaged"
type="button"
class="btn btn-remove btn-inverted append-right-8"
@click="showDiscardModal"
>
{{ __('Discard') }}
</button>
<button
:class="{
'btn-success': !isStaged,
'btn-warning': isStaged
}"
type="button"
class="btn btn-inverted"
@click="actionButtonClicked"
>
{{ activeButtonText }}
</button>
</div>
</div>
</template>
<script> <script>
import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
...@@ -9,6 +11,7 @@ export default { ...@@ -9,6 +11,7 @@ export default {
components: { components: {
Icon, Icon,
ListItem, ListItem,
GlModal,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -56,6 +59,11 @@ export default { ...@@ -56,6 +59,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
emptyStateText: {
type: String,
required: false,
default: __('No changes'),
},
}, },
computed: { computed: {
titleText() { titleText() {
...@@ -68,11 +76,19 @@ export default { ...@@ -68,11 +76,19 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
$(this.$refs.actionBtn).tooltip('hide');
},
openDiscardModal() {
$('#discard-all-changes').modal('show');
}, },
}, },
discardModalText: __(
"You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
),
}; };
</script> </script>
...@@ -81,27 +97,32 @@ export default { ...@@ -81,27 +97,32 @@ export default {
class="ide-commit-list-container" class="ide-commit-list-container"
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header d-flex mb-0"
> >
<div <div
class="multi-file-commit-panel-header-title" class="d-flex align-items-center flex-fill"
> >
<icon <icon
v-once v-once
:name="iconName" :name="iconName"
:size="18" :size="18"
class="append-right-8"
/> />
<strong>
{{ titleText }} {{ titleText }}
</strong>
<div class="d-flex ml-auto"> <div class="d-flex ml-auto">
<button <button
v-tooltip v-tooltip
v-show="filesLength" ref="actionBtn"
:title="actionBtnText"
:aria-label="actionBtnText"
:disabled="!filesLength"
:class="{ :class="{
'd-flex': filesLength 'disabled-content': !filesLength
}" }"
:title="actionBtnText"
type="button" type="button"
class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
data-placement="bottom" data-placement="bottom"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
...@@ -109,18 +130,32 @@ export default { ...@@ -109,18 +130,32 @@ export default {
> >
<icon <icon
:name="actionBtnIcon" :name="actionBtnIcon"
:size="12" :size="16"
class="ml-auto mr-auto" class="ml-auto mr-auto"
/> />
</button> </button>
<span <button
v-tooltip
v-if="!stagedList"
:title="__('Discard all changes')"
:aria-label="__('Discard all changes')"
:disabled="!filesLength"
:class="{ :class="{
'rounded-right': !filesLength 'disabled-content': !filesLength
}" }"
class="ide-commit-file-count order-0 rounded-left text-center" type="button"
class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
data-placement="bottom"
data-container="body"
data-boundary="viewport"
@click="openDiscardModal"
> >
{{ filesLength }} <icon
</span> :size="16"
name="remove-all"
class="ml-auto mr-auto"
/>
</button>
</div> </div>
</div> </div>
</header> </header>
...@@ -143,9 +178,19 @@ export default { ...@@ -143,9 +178,19 @@ export default {
</ul> </ul>
<p <p
v-else v-else
class="multi-file-commit-list form-text text-muted" class="multi-file-commit-list form-text text-muted text-center"
> >
{{ __('No changes') }} {{ emptyStateText }}
</p> </p>
<gl-modal
v-if="!stagedList"
id="discard-all-changes"
:footer-primary-button-text="__('Discard all changes')"
:header-title-text="__('Discard all unstaged changes?')"
footer-primary-button-variant="danger"
@submit="discardAllChanges"
>
{{ $options.discardModalText }}
</gl-modal>
</div> </div>
</template> </template>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants'; import { viewerTypes } from '../../constants';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
Icon, Icon,
StageButton, StageButton,
UnstageButton, UnstageButton,
FileIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -48,7 +50,7 @@ export default { ...@@ -48,7 +50,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`; return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
iconClass() { iconClass() {
return `${getCommitIconMap(this.file).class} append-right-8`; return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
}, },
fullKey() { fullKey() {
return `${this.keyPrefix}-${this.file.key}`; return `${this.keyPrefix}-${this.file.key}`;
...@@ -105,17 +107,24 @@ export default { ...@@ -105,17 +107,24 @@ export default {
@click="openFileInEditor" @click="openFileInEditor"
> >
<span class="multi-file-commit-list-file-path d-flex align-items-center"> <span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon
:file-name="file.name"
class="append-right-8"
/>{{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
<icon <icon
:name="iconName" :name="iconName"
:size="16" :size="16"
:css-classes="iconClass" :css-classes="iconClass"
/>{{ file.name }} />
</span>
</div> </div>
<component <component
:is="actionComponent" :is="actionComponent"
:path="file.path" :path="file.path"
class="d-flex position-absolute"
/> />
</div> </div>
</div>
</div>
</template> </template>
<script> <script>
import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default { export default {
components: { components: {
Icon, Icon,
GlModal,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -16,8 +20,22 @@ export default { ...@@ -16,8 +20,22 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
modalId() {
return `discard-file-${this.path}`;
},
modalTitle() {
return sprintf(
__('Discard changes to %{path}?'),
{ path: this.path },
);
},
},
methods: { methods: {
...mapActions(['stageChange', 'discardFileChanges']), ...mapActions(['stageChange', 'discardFileChanges']),
showDiscardModal() {
$(document.getElementById(this.modalId)).modal('show');
},
}, },
}; };
</script> </script>
...@@ -25,51 +43,50 @@ export default { ...@@ -25,51 +43,50 @@ export default {
<template> <template>
<div <div
v-once v-once
class="multi-file-discard-btn dropdown" class="multi-file-discard-btn d-flex"
> >
<button <button
v-tooltip v-tooltip
:aria-label="__('Stage changes')" :aria-label="__('Stage changes')"
:title="__('Stage changes')" :title="__('Stage changes')"
type="button" type="button"
class="btn btn-blank append-right-5 d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
@click.stop="stageChange(path)" @click.stop.prevent="stageChange(path)"
> >
<icon <icon
:size="12" :size="16"
name="mobile-issue-close" name="mobile-issue-close"
class="ml-auto mr-auto"
/> />
</button> </button>
<button <button
v-tooltip v-tooltip
:title="__('More actions')" :aria-label="__('Discard changes')"
:title="__('Discard changes')"
type="button" type="button"
class="btn btn-blank d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
data-toggle="dropdown" @click.stop.prevent="showDiscardModal"
data-display="static"
> >
<icon <icon
:size="12" :size="16"
name="ellipsis_h" name="remove"
class="ml-auto mr-auto"
/> />
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <gl-modal
<ul> :id="modalId"
<li> :header-title-text="modalTitle"
<button :footer-primary-button-text="__('Discard changes')"
type="button" footer-primary-button-variant="danger"
@click.stop="discardFileChanges(path)" @submit="discardFileChanges(path)"
> >
{{ __('Discard changes') }} {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
</button> </gl-modal>
</li>
</ul>
</div>
</div> </div>
</template> </template>
...@@ -25,22 +25,23 @@ export default { ...@@ -25,22 +25,23 @@ export default {
<template> <template>
<div <div
v-once v-once
class="multi-file-discard-btn" class="multi-file-discard-btn d-flex"
> >
<button <button
v-tooltip v-tooltip
:aria-label="__('Unstage changes')" :aria-label="__('Unstage changes')"
:title="__('Unstage changes')" :title="__('Unstage changes')"
type="button" type="button"
class="btn btn-blank d-flex align-items-center" class="btn btn-blank align-items-center"
data-container="body" data-container="body"
data-boundary="viewport" data-boundary="viewport"
data-placement="bottom" data-placement="bottom"
@click="unstageChange(path)" @click.stop.prevent="unstageChange(path)"
> >
<icon <icon
:size="12" :size="16"
name="history" name="redo"
class="ml-auto mr-auto"
/> />
</button> </button>
</div> </div>
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Dropdown from './dropdown.vue';
export default {
components: {
Dropdown,
},
computed: {
...mapGetters(['activeFile']),
...mapGetters('fileTemplates', ['templateTypes']),
...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
showTemplatesDropdown() {
return Object.keys(this.selectedTemplateType).length > 0;
},
},
watch: {
activeFile: 'setInitialType',
},
mounted() {
this.setInitialType();
},
methods: {
...mapActions('fileTemplates', [
'setSelectedTemplateType',
'fetchTemplate',
'undoFileTemplate',
]),
setInitialType() {
const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
if (initialTemplateType) {
this.setSelectedTemplateType(initialTemplateType);
}
},
selectTemplateType(templateType) {
this.setSelectedTemplateType(templateType);
},
selectTemplate(template) {
this.fetchTemplate(template);
},
undo() {
this.undoFileTemplate();
},
},
};
</script>
<template>
<div class="d-flex align-items-center ide-file-templates">
<strong class="append-right-default">
{{ __('File templates') }}
</strong>
<dropdown
:data="templateTypes"
:label="selectedTemplateType.name || __('Choose a type...')"
class="mr-2"
@click="selectTemplateType"
/>
<dropdown
v-if="showTemplatesDropdown"
:label="__('Choose a template...')"
:is-async-data="true"
:searchable="true"
:title="__('File templates')"
class="mr-2"
@click="selectTemplate"
/>
<transition name="fade">
<button
v-show="updateSuccess"
type="button"
class="btn btn-default"
@click="undo"
>
{{ __('Undo') }}
</button>
</transition>
</div>
</template>
<script>
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
LoadingIcon,
},
props: {
data: {
type: Array,
required: false,
default: () => [],
},
label: {
type: String,
required: true,
},
title: {
type: String,
required: false,
default: null,
},
isAsyncData: {
type: Boolean,
required: false,
default: false,
},
searchable: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
search: '',
};
},
computed: {
...mapState('fileTemplates', ['templates', 'isLoading']),
outputData() {
return (this.isAsyncData ? this.templates : this.data).filter(t => {
if (!this.searchable) return true;
return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
});
},
showLoading() {
return this.isAsyncData ? this.isLoading : false;
},
},
mounted() {
$(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
},
beforeDestroy() {
$(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
},
methods: {
...mapActions('fileTemplates', ['fetchTemplateTypes']),
fetchTemplatesIfAsync() {
if (this.isAsyncData) {
this.fetchTemplateTypes();
}
},
clickItem(item) {
this.$emit('click', item);
},
},
};
</script>
<template>
<div class="dropdown">
<dropdown-button
:toggle-text="label"
data-display="static"
/>
<div class="dropdown-menu pb-0">
<div
v-if="title"
class="dropdown-title ml-0 mr-0"
>
{{ title }}
</div>
<div
v-if="!showLoading && searchable"
class="dropdown-input"
>
<input
v-model="search"
:placeholder="__('Filter...')"
type="search"
class="dropdown-input-field"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
</div>
<div class="dropdown-content">
<loading-icon
v-if="showLoading"
size="2"
/>
<ul v-else>
<li
v-for="(item, index) in outputData"
:key="index"
>
<button
type="button"
@click="clickItem(item)"
>
{{ item.name }}
</button>
</li>
</ul>
</div>
</div>
</div>
</template>
...@@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue'; ...@@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue'; import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue'; import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue'; import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
FindFile, FindFile,
RightPane, RightPane,
ErrorMessage, ErrorMessage,
CommitEditorHeader,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -34,7 +36,7 @@ export default { ...@@ -34,7 +36,7 @@ export default {
'currentProjectId', 'currentProjectId',
'errorMessage', 'errorMessage',
]), ]),
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']), ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
}, },
mounted() { mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e); window.onbeforeunload = e => this.onBeforeUnload(e);
...@@ -96,7 +98,12 @@ export default { ...@@ -96,7 +98,12 @@ export default {
<template <template
v-if="activeFile" v-if="activeFile"
> >
<commit-editor-header
v-if="isCommitModeActive"
:active-file="activeFile"
/>
<repo-tabs <repo-tabs
v-else
:active-file="activeFile" :active-file="activeFile"
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
......
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants'; import { modalTypes } from '../../constants';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
}, },
computed: { computed: {
...mapState(['entryModal']), ...mapState(['entryModal']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: { entryName: {
get() { get() {
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
...@@ -31,7 +33,9 @@ export default { ...@@ -31,7 +33,9 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory'); return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) { } else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} }
return __('Create new file'); return __('Create new file');
...@@ -40,11 +44,16 @@ export default { ...@@ -40,11 +44,16 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create directory'); return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) { } else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} }
return __('Create file'); return __('Create file');
}, },
isCreatingNew() {
return this.entryModal.type !== modalTypes.rename;
},
}, },
methods: { methods: {
...mapActions(['createTempEntry', 'renameEntry']), ...mapActions(['createTempEntry', 'renameEntry']),
...@@ -61,6 +70,14 @@ export default { ...@@ -61,6 +70,14 @@ export default {
}); });
} }
}, },
createFromTemplate(template) {
this.createTempEntry({
name: template.name,
type: this.entryModal.type,
});
$('#ide-new-entry').modal('toggle');
},
focusInput() { focusInput() {
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
}, },
...@@ -77,6 +94,7 @@ export default { ...@@ -77,6 +94,7 @@ export default {
:header-title-text="modalTitle" :header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel" :footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success" footer-primary-button-variant="success"
modal-size="lg"
@submit="submitForm" @submit="submitForm"
@open="focusInput" @open="focusInput"
@closed="closedModal" @closed="closedModal"
...@@ -84,16 +102,35 @@ export default { ...@@ -84,16 +102,35 @@ export default {
<div <div
class="form-group row" class="form-group row"
> >
<label class="label-bold col-form-label col-sm-3"> <label class="label-bold col-form-label col-sm-2">
{{ __('Name') }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-10">
<input <input
ref="fieldName" ref="fieldName"
v-model="entryName" v-model="entryName"
type="text" type="text"
class="form-control" class="form-control"
placeholder="/dir/file_name"
/> />
<ul
v-if="isCreatingNew"
class="prepend-top-default list-inline"
>
<li
v-for="(template, index) in templateTypes"
:key="index"
class="list-inline-item"
>
<button
type="button"
class="btn btn-missing p-1 pr-2 pl-2"
@click="createFromTemplate(template)"
>
{{ template.name }}
</button>
</li>
</ul>
</div> </div>
</div> </div>
</gl-modal> </gl-modal>
......
...@@ -95,8 +95,9 @@ export default { ...@@ -95,8 +95,9 @@ export default {
:file-list="changedFiles" :file-list="changedFiles"
:action-btn-text="__('Stage all changes')" :action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
:empty-state-text="__('There are no unstaged changes')"
action="stageAllChanges" action="stageAllChanges"
action-btn-icon="mobile-issue-close" action-btn-icon="stage-all"
item-action-component="stage-button" item-action-component="stage-button"
class="is-first" class="is-first"
icon-name="unstaged" icon-name="unstaged"
...@@ -108,8 +109,9 @@ export default { ...@@ -108,8 +109,9 @@ export default {
:action-btn-text="__('Unstage all changes')" :action-btn-text="__('Unstage all changes')"
:staged-list="true" :staged-list="true"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
:empty-state-text="__('There are no staged changes')"
action="unstageAllChanges" action="unstageAllChanges"
action-btn-icon="history" action-btn-icon="unstage-all"
item-action-component="unstage-button" item-action-component="unstage-button"
icon-name="staged" icon-name="staged"
/> />
......
...@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; ...@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
export default { export default {
components: { components: {
ContentViewer, ContentViewer,
DiffViewer, DiffViewer,
ExternalLink, ExternalLink,
FileTemplatesBar,
}, },
props: { props: {
file: { file: {
...@@ -34,6 +36,7 @@ export default { ...@@ -34,6 +36,7 @@ export default {
'isCommitModeActive', 'isCommitModeActive',
'isReviewModeActive', 'isReviewModeActive',
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
...@@ -216,7 +219,7 @@ export default { ...@@ -216,7 +219,7 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix" > <div class="ide-mode-tabs clearfix">
<ul <ul
v-if="!shouldHideEditor && isEditModeActive" v-if="!shouldHideEditor && isEditModeActive"
class="nav-links float-left" class="nav-links float-left"
...@@ -249,6 +252,9 @@ export default { ...@@ -249,6 +252,9 @@ export default {
:file="file" :file="file"
/> />
</div> </div>
<file-templates-bar
v-if="showFileTemplatesBar(file.name)"
/>
<div <div
v-show="!shouldHideEditor && file.viewMode ==='editor'" v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor" ref="editor"
......
...@@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker'; import FilesDecoratorWorker from './workers/files_decorator_worker';
import { stageKeys } from '../constants';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
...@@ -122,14 +123,28 @@ export const scrollToTab = () => { ...@@ -122,14 +123,28 @@ export const scrollToTab = () => {
}); });
}; };
export const stageAllChanges = ({ state, commit }) => { export const stageAllChanges = ({ state, commit, dispatch }) => {
const openFile = state.openFiles[0];
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
dispatch('openPendingTab', {
file: state.stagedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.staged,
});
}; };
export const unstageAllChanges = ({ state, commit }) => { export const unstageAllChanges = ({ state, commit, dispatch }) => {
const openFile = state.openFiles[0];
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
dispatch('openPendingTab', {
file: state.changedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.unstaged,
});
}; };
export const updateViewer = ({ commit }, viewer) => { export const updateViewer = ({ commit }, viewer) => {
...@@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); ...@@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path]; const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath }); commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') { if (entry.type === 'tree') {
...@@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath ...@@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
); );
} }
if (!entryPath) { if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path); dispatch('deleteEntry', path);
} }
}; };
......
...@@ -5,7 +5,7 @@ import service from '../../services'; ...@@ -5,7 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants'; import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const { path } = file; const { path } = file;
...@@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = ...@@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
}; };
export const stageChange = ({ commit, state }, path) => { export const stageChange = ({ commit, state, dispatch }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const openFile = state.openFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
...@@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => { ...@@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
if (stagedFile) { if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
} }
if (openFile && openFile.active) {
const file = state.stagedFiles.find(f => f.path === path);
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
});
}
}; };
export const unstageChange = ({ commit }, path) => { export const unstageChange = ({ commit, dispatch, state }, path) => {
const openFile = state.openFiles.find(f => f.path === path);
commit(types.UNSTAGE_CHANGE, path); commit(types.UNSTAGE_CHANGE, path);
if (openFile && openFile.active) {
const file = state.changedFiles.find(f => f.path === path);
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
});
}
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix }); commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`); router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
return true; return true;
......
...@@ -8,6 +8,7 @@ import commitModule from './modules/commit'; ...@@ -8,6 +8,7 @@ import commitModule from './modules/commit';
import pipelines from './modules/pipelines'; import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests'; import mergeRequests from './modules/merge_requests';
import branches from './modules/branches'; import branches from './modules/branches';
import fileTemplates from './modules/file_templates';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -22,6 +23,7 @@ export const createStore = () => ...@@ -22,6 +23,7 @@ export const createStore = () =>
pipelines, pipelines,
mergeRequests, mergeRequests,
branches, branches,
fileTemplates: fileTemplates(),
}, },
}); });
......
import Api from '~/api'; import Api from '~/api';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
import eventHub from '../../../eventhub';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => { export const receiveTemplateTypesError = ({ commit, dispatch }) => {
...@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => { ...@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
.catch(() => dispatch('receiveTemplateTypesError')); .catch(() => dispatch('receiveTemplateTypesError'));
}; };
export const setSelectedTemplateType = ({ commit }, type) => export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
commit(types.SET_SELECTED_TEMPLATE_TYPE, type); commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
if (rootGetters.activeFile.prevPath === type.name) {
dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true });
} else if (rootGetters.activeFile.name !== type.name) {
dispatch(
'renameEntry',
{
path: rootGetters.activeFile.path,
name: type.name,
},
{ root: true },
);
}
};
export const receiveTemplateError = ({ dispatch }, template) => { export const receiveTemplateError = ({ dispatch }, template) => {
dispatch( dispatch(
'setErrorMessage', 'setErrorMessage',
...@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => ...@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
{ root: true }, { root: true },
); );
commit(types.SET_UPDATE_SUCCESS, true); commit(types.SET_UPDATE_SUCCESS, true);
eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content);
}; };
export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
...@@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { ...@@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
commit(types.SET_UPDATE_SUCCESS, false); commit(types.SET_UPDATE_SUCCESS, false);
eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw);
if (file.prevPath) {
dispatch('discardFileChanges', file.path, { root: true });
}
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
import { activityBarViews } from '../../../constants';
export const templateTypes = () => [ export const templateTypes = () => [
{ {
name: '.gitlab-ci.yml', name: '.gitlab-ci.yml',
...@@ -17,7 +19,8 @@ export const templateTypes = () => [ ...@@ -17,7 +19,8 @@ export const templateTypes = () => [
}, },
]; ];
export const showFileTemplatesBar = (_, getters) => name => export const showFileTemplatesBar = (_, getters, rootState) => name =>
getters.templateTypes.find(t => t.name === name); getters.templateTypes.find(t => t.name === name) &&
rootState.currentActivityView === activityBarViews.edit;
export default () => {}; export default () => {};
...@@ -3,10 +3,10 @@ import * as actions from './actions'; ...@@ -3,10 +3,10 @@ import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
export default { export default () => ({
namespaced: true, namespaced: true,
actions, actions,
state: createState(), state: createState(),
getters, getters,
mutations, mutations,
}; });
import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request'; import mergeRequestMutation from './mutations/merge_request';
...@@ -226,7 +227,7 @@ export default { ...@@ -226,7 +227,7 @@ export default {
path: newPath, path: newPath,
name: entryPath ? oldEntry.name : name, name: entryPath ? oldEntry.name : name,
tempFile: true, tempFile: true,
prevPath: oldEntry.path, prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [], tree: [],
parentPath, parentPath,
...@@ -245,6 +246,20 @@ export default { ...@@ -245,6 +246,20 @@ export default {
if (newEntry.type === 'blob') { if (newEntry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(newEntry); state.changedFiles = state.changedFiles.concat(newEntry);
} }
if (state.entries[newPath].opened) {
state.openFiles.push(state.entries[newPath]);
}
if (oldEntry.tempFile) {
const filterMethod = f => f.path !== oldEntry.path;
state.openFiles = state.openFiles.filter(filterMethod);
state.changedFiles = state.changedFiles.filter(filterMethod);
parent.tree = parent.tree.filter(filterMethod);
Vue.delete(state.entries, oldEntry.path);
}
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
......
...@@ -55,7 +55,7 @@ export default { ...@@ -55,7 +55,7 @@ export default {
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath), f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
); );
if (file.tempFile) { if (file.tempFile && file.content === '') {
Object.assign(state.entries[file.path], { Object.assign(state.entries[file.path], {
content: raw, content: raw,
}); });
......
...@@ -82,11 +82,12 @@ export default { ...@@ -82,11 +82,12 @@ export default {
value: 0, value: 0,
}, },
currentXCoordinate: 0, currentXCoordinate: 0,
currentCoordinates: [], currentCoordinates: {},
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
timeSeries: [], timeSeries: [],
realPixelRatio: 1, realPixelRatio: 1,
seriesUnderMouse: [],
}; };
}, },
computed: { computed: {
...@@ -126,6 +127,9 @@ export default { ...@@ -126,6 +127,9 @@ export default {
this.draw(); this.draw();
}, },
methods: { methods: {
showDot(path) {
return this.showFlagContent && this.seriesUnderMouse.includes(path);
},
draw() { draw() {
const breakpointSize = bp.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0]; const query = this.graphData.queries[0];
...@@ -155,7 +159,24 @@ export default { ...@@ -155,7 +159,24 @@ export default {
point.y = e.clientY; point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x += 7; point.x += 7;
const firstTimeSeries = this.timeSeries[0];
this.seriesUnderMouse = this.timeSeries.filter((series) => {
const mouseX = series.timeSeriesScaleX.invert(point.x);
let minDistance = Infinity;
const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
if (distance < minDistance) {
minDistance = distance;
return x;
}
return closest;
});
return series.values.find(v => v.time.toString() === closestTickMark);
});
const firstTimeSeries = this.seriesUnderMouse[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d0 = firstTimeSeries.values[overlayIndex - 1]; const d0 = firstTimeSeries.values[overlayIndex - 1];
...@@ -190,6 +211,17 @@ export default { ...@@ -190,6 +211,17 @@ export default {
axisXScale.domain(d3.extent(allValues, d => d.time)); axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
const seriesKeys = {};
series.values.forEach(v => {
seriesKeys[v.time] = true;
});
return {
...obj,
...seriesKeys,
};
}, {});
const xAxis = d3 const xAxis = d3
.axisBottom() .axisBottom()
.scale(axisXScale) .scale(axisXScale)
...@@ -277,9 +309,8 @@ export default { ...@@ -277,9 +309,8 @@ export default {
:line-style="path.lineStyle" :line-style="path.lineStyle"
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
:current-coordinates="currentCoordinates[index]" :current-coordinates="currentCoordinates[path.metricTag]"
:current-time-series-index="index" :show-dot="showDot(path)"
:show-dot="showFlagContent"
/> />
<graph-deployment <graph-deployment
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -303,7 +334,7 @@ export default { ...@@ -303,7 +334,7 @@ export default {
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries" :time-series="seriesUnderMouse"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:legend-title="legendTitle" :legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData" :deployment-flag-data="deploymentFlagData"
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
required: true, required: true,
}, },
currentCoordinates: { currentCoordinates: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
...@@ -91,8 +91,8 @@ export default { ...@@ -91,8 +91,8 @@ export default {
}, },
methods: { methods: {
seriesMetricValue(seriesIndex, series) { seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[seriesIndex] const indexFromCoordinates = this.currentCoordinates[series.metricTag]
? this.currentCoordinates[seriesIndex].currentDataIndex : 0; ? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
const index = this.deploymentFlagData const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex ? this.deploymentFlagData.seriesIndex
: indexFromCoordinates; : indexFromCoordinates;
......
...@@ -50,19 +50,24 @@ const mixins = { ...@@ -50,19 +50,24 @@ const mixins = {
}, },
positionFlag() { positionFlag() {
const timeSeries = this.timeSeries[0]; const timeSeries = this.seriesUnderMouse[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); if (!timeSeries) {
return;
}
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
this.currentData = timeSeries.values[hoveredDataIndex]; this.currentData = timeSeries.values[hoveredDataIndex];
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
this.currentCoordinates = this.timeSeries.map((series) => { this.currentCoordinates = {};
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
this.seriesUnderMouse.forEach((series) => {
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
const currentData = series.values[currentDataIndex]; const currentData = series.values[currentDataIndex];
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
return { this.currentCoordinates[series.metricTag] = {
currentX, currentX,
currentY, currentY,
currentDataIndex, currentDataIndex,
......
...@@ -2,7 +2,7 @@ import _ from 'underscore'; ...@@ -2,7 +2,7 @@ import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale'; import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape'; import { line, area, curveLinear } from 'd3-shape';
import { extent, max, sum } from 'd3-array'; import { extent, max, sum } from 'd3-array';
import { timeMinute } from 'd3-time'; import { timeMinute, timeSecond } from 'd3-time';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const d3 = { const d3 = {
...@@ -14,6 +14,7 @@ const d3 = { ...@@ -14,6 +14,7 @@ const d3 = {
extent, extent,
max, max,
timeMinute, timeMinute,
timeSecond,
sum, sum,
}; };
...@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick]; return defaultColorPalette[pick];
} }
function findByDate(series, time) {
const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
if (val) {
return val.value;
}
return NaN;
}
// The timeseries data may have gaps in it
// but we need a regularly-spaced set of time/value pairs
// this gives us a complete range of one minute intervals
// offset the same amount as the original data
const [minX, maxX] = xDom;
const offset = d3.timeMinute(minX) - Number(minX);
const datesWithoutGaps = d3.timeSecond.every(60)
.range(d3.timeMinute.offset(minX, -1), maxX)
.map(d => d - offset);
query.result.forEach((timeSeries, timeSeriesNumber) => { query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = ''; let metricTag = '';
let lineColor = ''; let lineColor = '';
...@@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
}); });
} }
const values = datesWithoutGaps.map(time => ({
time,
value: findByDate(timeSeries.values, time),
}));
timeSeriesParsed.push({ timeSeriesParsed.push({
linePath: lineFunction(timeSeries.values), linePath: lineFunction(values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(values),
timeSeriesScaleX, timeSeriesScaleX,
timeSeriesScaleY, timeSeriesScaleY,
values: timeSeries.values, values: timeSeries.values,
......
...@@ -166,6 +166,10 @@ ...@@ -166,6 +166,10 @@
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
} }
&.btn-warning {
@include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
}
&.btn-primary, &.btn-primary,
&.btn-info { &.btn-info {
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
......
...@@ -7,6 +7,8 @@ $ide-context-header-padding: 10px; ...@@ -7,6 +7,8 @@ $ide-context-header-padding: 10px;
$ide-project-avatar-end: $ide-context-header-padding + 48px; $ide-project-avatar-end: $ide-context-header-padding + 48px;
$ide-tree-padding: $gl-padding; $ide-tree-padding: $gl-padding;
$ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
$ide-commit-row-height: 32px;
$ide-commit-header-height: 48px;
.project-refs-form, .project-refs-form,
.project-refs-target-form { .project-refs-target-form {
...@@ -567,24 +569,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -567,24 +569,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
} }
.multi-file-commit-panel-header { .multi-file-commit-panel-header {
display: flex; height: $ide-commit-header-height;
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: 12px 0; padding: 12px 0;
} }
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
align-items: center;
svg {
margin-right: $gl-btn-padding;
color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn { .multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark; border-left: 1px solid $white-dark;
margin-left: auto; margin-left: auto;
...@@ -594,8 +583,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -594,8 +583,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: $grid-size 0; padding: $grid-size 0;
margin-left: -$grid-size;
margin-right: -$grid-size;
min-height: 60px; min-height: 60px;
&.form-text.text-muted { &.form-text.text-muted {
...@@ -660,6 +647,8 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -660,6 +647,8 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
.multi-file-commit-list-path { .multi-file-commit-list-path {
cursor: pointer; cursor: pointer;
height: $ide-commit-row-height;
padding-right: 0;
&.is-active { &.is-active {
background-color: $white-normal; background-color: $white-normal;
...@@ -668,6 +657,12 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -668,6 +657,12 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
&:hover, &:hover,
&:focus { &:focus {
outline: 0; outline: 0;
.multi-file-discard-btn {
> .btn {
display: flex;
}
}
} }
svg { svg {
...@@ -679,6 +674,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -679,6 +674,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
.multi-file-commit-list-file-path { .multi-file-commit-list-file-path {
@include str-truncated(calc(100% - 30px)); @include str-truncated(calc(100% - 30px));
user-select: none;
&:active { &:active {
text-decoration: none; text-decoration: none;
...@@ -686,9 +682,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -686,9 +682,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
} }
.multi-file-discard-btn { .multi-file-discard-btn {
top: 4px; > .btn {
right: 8px; display: none;
bottom: 4px; width: $ide-commit-row-height;
height: $ide-commit-row-height;
}
svg { svg {
top: 0; top: 0;
...@@ -807,10 +805,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -807,10 +805,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
} }
.ide-staged-action-btn { .ide-staged-action-btn {
width: 22px; width: $ide-commit-row-height;
margin-left: -1px; height: $ide-commit-row-height;
border-top-left-radius: 0; color: inherit;
border-bottom-left-radius: 0;
> svg { > svg {
top: 0; top: 0;
...@@ -1442,3 +1439,29 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1442,3 +1439,29 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
.ide-file-templates {
padding: $grid-size $gl-padding;
background-color: $gray-light;
border-bottom: 1px solid $white-dark;
.dropdown {
min-width: 180px;
}
.dropdown-content {
max-height: 222px;
}
}
.ide-commit-editor-header {
height: 65px;
padding: 8px 16px;
background-color: $theme-gray-50;
box-shadow: inset 0 -1px $white-dark;
}
.ide-commit-list-changed-icon {
width: $ide-commit-row-height;
height: $ide-commit-row-height;
}
...@@ -14,7 +14,8 @@ class Admin::LogsController < Admin::ApplicationController ...@@ -14,7 +14,8 @@ class Admin::LogsController < Admin::ApplicationController
Gitlab::GitLogger, Gitlab::GitLogger,
Gitlab::EnvironmentLogger, Gitlab::EnvironmentLogger,
Gitlab::SidekiqLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger Gitlab::RepositoryCheckLogger,
Gitlab::ProjectServiceLogger
] ]
end end
end end
...@@ -101,6 +101,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -101,6 +101,7 @@ class ProfilesController < Profiles::ApplicationController
:organization, :organization,
:preferred_language, :preferred_language,
:private_profile, :private_profile,
:include_private_contributions,
status: [:emoji, :message] status: [:emoji, :message]
) )
end end
......
...@@ -21,6 +21,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -21,6 +21,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs def render_diffs
@environment = @merge_request.environments_for(current_user).last @environment = @merge_request.environments_for(current_user).last
@diffs.write_cache
render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes) render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
end end
......
...@@ -48,20 +48,6 @@ class UserRecentEventsFinder ...@@ -48,20 +48,6 @@ class UserRecentEventsFinder
end end
def projects def projects
# Compile a list of projects `current_user` interacted with target_user.project_interactions.to_sql
# and `target_user` is allowed to see.
authorized = target_user
.project_interactions
.joins(:project_authorizations)
.where(project_authorizations: { user: current_user })
.select(:id)
visible = target_user
.project_interactions
.where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
.select(:id)
Gitlab::SQL::Union.new([authorized, visible]).to_sql
end end
end end
...@@ -19,7 +19,7 @@ module EventsHelper ...@@ -19,7 +19,7 @@ module EventsHelper
name = self_added ? 'You' : author.name name = self_added ? 'You' : author.name
link_to name, user_path(author.username), title: name link_to name, user_path(author.username), title: name
else else
event.author_name escape_once(event.author_name)
end end
end end
......
...@@ -111,6 +111,10 @@ module Issuable ...@@ -111,6 +111,10 @@ module Issuable
def allows_multiple_assignees? def allows_multiple_assignees?
false false
end end
def has_multiple_assignees?
assignees.count > 1
end
end end
class_methods do class_methods do
......
module ProjectServicesLoggable
def log_info(message, params = {})
message = build_message(message, params)
logger.info(message)
end
def log_error(message, params = {})
message = build_message(message, params)
logger.error(message)
end
def build_message(message, params = {})
{
service_class: self.class.name,
project_id: project.id,
project_path: project.full_path,
message: message
}.merge(params)
end
def logger
Gitlab::ProjectServiceLogger
end
end
...@@ -157,15 +157,17 @@ class Event < ActiveRecord::Base ...@@ -157,15 +157,17 @@ class Event < ActiveRecord::Base
if push? || commit_note? if push? || commit_note?
Ability.allowed?(user, :download_code, project) Ability.allowed?(user, :download_code, project)
elsif membership_changed? elsif membership_changed?
true Ability.allowed?(user, :read_project, project)
elsif created_project? elsif created_project?
true Ability.allowed?(user, :read_project, project)
elsif issue? || issue_note? elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target) Ability.allowed?(user, :read_issue, note? ? note_target : target)
elsif merge_request? || merge_request_note? elsif merge_request? || merge_request_note?
Ability.allowed?(user, :read_merge_request, note? ? note_target : target) Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
elsif milestone?
Ability.allowed?(user, :read_project, project)
else else
milestone? false # No other event types are visible
end end
end end
......
...@@ -101,7 +101,7 @@ http://app.asana.com/-/account_api' ...@@ -101,7 +101,7 @@ http://app.asana.com/-/account_api'
task.update(completed: true) task.update(completed: true)
end end
rescue => e rescue => e
Rails.logger.error(e.message) log_error(e.message)
next next
end end
end end
......
...@@ -104,7 +104,7 @@ class IrkerService < Service ...@@ -104,7 +104,7 @@ class IrkerService < Service
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient)) uri = consider_uri(URI.parse(new_recipient))
rescue rescue
Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}") log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
end end
end end
......
...@@ -92,7 +92,7 @@ class IssueTrackerService < Service ...@@ -92,7 +92,7 @@ class IssueTrackerService < Service
rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end end
Rails.logger.info(message) log_info(message)
result result
end end
......
...@@ -205,7 +205,7 @@ class JiraService < IssueTrackerService ...@@ -205,7 +205,7 @@ class JiraService < IssueTrackerService
begin begin
issue.transitions.build.save!(transition: { id: transition_id }) issue.transitions.build.save!(transition: { id: transition_id })
rescue => error rescue => error
Rails.logger.info "#{self.class.name} Issue Transition failed message ERROR: #{client_url} - #{error.message}" log_error("Issue transition failed", error: error.message, client_url: client_url)
return false return false
end end
end end
...@@ -257,9 +257,8 @@ class JiraService < IssueTrackerService ...@@ -257,9 +257,8 @@ class JiraService < IssueTrackerService
new_remote_link.save!(remote_link_props) new_remote_link.save!(remote_link_props)
end end
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}." log_info("Successfully posted", client_url: client_url)
Rails.logger.info(result_message) "SUCCESS: Successfully posted to http://jira.example.net."
result_message
end end
end end
...@@ -317,7 +316,7 @@ class JiraService < IssueTrackerService ...@@ -317,7 +316,7 @@ class JiraService < IssueTrackerService
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
@error = e.message @error = e.message
Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}" log_error("Error sending message", client_url: client_url, error: @error)
nil nil
end end
......
...@@ -6,6 +6,7 @@ class Service < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Service < ActiveRecord::Base
prepend EE::Service prepend EE::Service
include Sortable include Sortable
include Importable include Importable
include ProjectServicesLoggable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
......
...@@ -30,7 +30,7 @@ module MergeRequests ...@@ -30,7 +30,7 @@ module MergeRequests
def clear_cache(new_diff) def clear_cache(new_diff)
# Executing the iteration we cache highlighted diffs for each diff file of # Executing the iteration we cache highlighted diffs for each diff file of
# MergeRequestDiff. # MergeRequestDiff.
new_diff.diffs_collection.diff_files.to_a new_diff.diffs_collection.write_cache
# Remove cache for all diffs on this MR. Do not use the association on the # Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when # model, as that will interfere with other actions happening when
...@@ -38,7 +38,7 @@ module MergeRequests ...@@ -38,7 +38,7 @@ module MergeRequests
MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
next if merge_request_diff == new_diff next if merge_request_diff == new_diff
merge_request_diff.diffs_collection.clear_cache! merge_request_diff.diffs_collection.clear_cache
end end
end end
end end
......
...@@ -11,7 +11,7 @@ module Wikis ...@@ -11,7 +11,7 @@ module Wikis
def initialize(*args) def initialize(*args)
super super
@file_name = truncate_file_name(params[:file_name]) @file_name = clean_file_name(params[:file_name])
@file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
@commit_message ||= "Upload attachment #{@file_name}" @commit_message ||= "Upload attachment #{@file_name}"
@branch_name ||= wiki.default_branch @branch_name ||= wiki.default_branch
...@@ -23,8 +23,16 @@ module Wikis ...@@ -23,8 +23,16 @@ module Wikis
private private
def truncate_file_name(file_name) def clean_file_name(file_name)
return unless file_name.present? return unless file_name.present?
file_name = truncate_file_name(file_name)
# CommonMark does not allow Urls with whitespaces, so we have to replace them
# Using the same regex Carrierwave use to replace invalid characters
file_name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, '_')
end
def truncate_file_name(file_name)
return file_name if file_name.length <= MAX_FILENAME_LENGTH return file_name if file_name.length <= MAX_FILENAME_LENGTH
extension = File.extname(file_name) extension = File.extname(file_name)
......
...@@ -6,8 +6,15 @@ class NamespaceFileUploader < FileUploader ...@@ -6,8 +6,15 @@ class NamespaceFileUploader < FileUploader
options.storage_path options.storage_path
end end
def self.base_dir(model, _store = nil) def self.base_dir(model, store = nil)
File.join(options.base_dir, 'namespace', model_path_segment(model)) base_dirs(model)[store || Store::LOCAL]
end
def self.base_dirs(model)
{
Store::LOCAL => File.join(options.base_dir, 'namespace', model_path_segment(model)),
Store::REMOTE => File.join('namespace', model_path_segment(model))
}
end end
def self.model_path_segment(model) def self.model_path_segment(model)
...@@ -18,11 +25,4 @@ class NamespaceFileUploader < FileUploader ...@@ -18,11 +25,4 @@ class NamespaceFileUploader < FileUploader
def store_dir def store_dir
store_dirs[object_store] store_dirs[object_store]
end end
def store_dirs
{
Store::LOCAL => File.join(base_dir, dynamic_segment),
Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
}
end
end end
...@@ -11,3 +11,5 @@ ...@@ -11,3 +11,5 @@
= render "events/event/note", event: event = render "events/event/note", event: event
- else - else
= render "events/event/common", event: event = render "events/event/common", event: event
- elsif @user.include_private_contributions?
= render "events/event/private", event: event
%span.event-scope %span.event-scope
= event_preposition(event) = event_preposition(event)
- if event.project - if event.project
= link_to_project event.project = link_to_project(event.project)
- else - else
= event.project_name = event.project_name
= icon_for_profile_event(event) = icon_for_profile_event(event)
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author(event)
%span{ class: event.action_name } %span{ class: event.action_name }
- if event.target - if event.target
= event.action_name = event.action_name
......
= icon_for_profile_event(event) = icon_for_profile_event(event)
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author(event)
%span{ class: event.action_name } %span{ class: event.action_name }
= event_action_name(event) = event_action_name(event)
- if event.project - if event.project
= link_to_project event.project = link_to_project(event.project)
- else - else
= event.project_name = event.project_name
= icon_for_profile_event(event) = icon_for_profile_event(event)
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author(event)
= event.action_name = event.action_name
= event_note_title_html(event) = event_note_title_html(event)
......
.event-inline.event-item
.event-item-timestamp
= time_ago_with_tooltip(event.created_at)
.system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon')
.event-title
- author_name = capture do
%span.author_name= link_to_author(event)
= s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= icon_for_profile_event(event) = icon_for_profile_event(event)
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author(event)
%span.pushed #{event.action_name} #{event.ref_type} %span.pushed #{event.action_name} #{event.ref_type}
%strong %strong
- commits_link = project_commits_path(project, event.ref_name) - commits_link = project_commits_path(project, event.ref_name)
......
- breadcrumb_title "Edit Profile" - breadcrumb_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user) = form_errors(@user)
...@@ -7,34 +8,36 @@ ...@@ -7,34 +8,36 @@
.row .row
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Public Avatar = s_("Profiles|Public Avatar")
%p %p
- if @user.avatar? - if @user.avatar?
You can change your avatar here
- if gravatar_enabled? - if gravatar_enabled?
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} = s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can change your avatar here")
- else - else
You can upload an avatar here
- if gravatar_enabled? - if gravatar_enabled?
or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} = s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can upload your avatar here")
.col-lg-8 .col-lg-8
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160' = image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0= _("Upload new avatar") %h5.prepend-top-0= s_("Profiles|Upload new avatar")
.prepend-top-5.append-bottom-10 .prepend-top-5.append-bottom-10
%button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") %button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
%span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen") %span.avatar-file-name.prepend-left-default.js-avatar-filename= s_("Profiles|No file chosen")
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.form-text.text-muted= _("The maximum file size allowed is 200KB.") .form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar? - if @user.avatar?
%hr %hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted' = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'btn btn-danger btn-inverted'
%hr %hr
.row .row
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0= s_("User|Current status") %h4.prepend-top-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.") %p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8 .col-lg-8
= f.fields_for :status, @user.status do |status_form| = f.fields_for :status, @user.status do |status_form|
...@@ -66,62 +69,66 @@ ...@@ -66,62 +69,66 @@
.row .row
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Main settings = s_("Profiles|Main settings")
%p %p
This information will appear on your profile. = s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user? - if current_user.ldap_user?
Some options are unavailable for LDAP accounts = s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8 .col-lg-8
.row .row
- if @user.read_only_attribute?(:name) - if @user.read_only_attribute?(:name)
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) }
- else - else
= f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email) - if @user.read_only_attribute?(:email)
= f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account." = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) }
- else - else
= f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?), = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
help: user_email_help_text(@user) help: user_email_help_text(@user)
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
{ help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' }, { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2' control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{ help: 'This feature is experimental and translations are not complete yet.' }, { help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
control_class: 'select2' control_class: 'select2'
= f.text_field :skype = f.text_field :skype
= f.text_field :linkedin = f.text_field :linkedin
= f.text_field :twitter = f.text_field :twitter
= f.text_field :website_url, label: 'Website' = f.text_field :website_url, label: s_("Profiles|Website")
- if @user.read_only_attribute?(:location) - if @user.read_only_attribute?(:location)
= f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account." = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) }
- else - else
= f.text_field :location = f.text_field :location
= f.text_field :organization = f.text_field :organization
= f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.' = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr %hr
%h5 Private profile %h5= ("Private profile")
- private_profile_label = capture do - private_profile_label = capture do
Don't display activity-related personal information on your profile = s_("Profiles|Don't display activity-related personal information on your profiles")
= link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile') = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
= f.check_box :private_profile, label: private_profile_label = f.check_box :private_profile, label: private_profile_label
%h5= s_("Profiles|Private contributions")
= f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
.help-block
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
.prepend-top-default.append-bottom-default .prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: 'btn btn-success' = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success'
= link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel' = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel'
.modal.modal-profile-crop .modal.modal-profile-crop
.modal-dialog .modal-dialog
.modal-content .modal-content
.modal-header .modal-header
%h4.modal-title %h4.modal-title
Position and size your new avatar = s_("Profiles|Position and size your new avatar")
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
%span{ "aria-hidden": true } &times; %span{ "aria-hidden": true } &times;
.modal-body .modal-body
.profile-crop-image-container .profile-crop-image-container
%img.modal-profile-crop-image{ alt: 'Avatar cropper' } %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
.crop-controls .crop-controls
.btn-group .btn-group
%button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } } %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
...@@ -130,4 +137,4 @@ ...@@ -130,4 +137,4 @@
%span.fa.fa-search-minus %span.fa.fa-search-minus
.modal-footer .modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ type: 'button' } %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
Set new profile picture = s_("Profiles|Set new profile picture")
...@@ -20,17 +20,17 @@ ...@@ -20,17 +20,17 @@
- if !membership_locked? && @project.allowed_to_share_with_group? - if !membership_locked? && @project.allowed_to_share_with_group?
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' } %li.nav-tab{ role: 'presentation' }
%a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
%li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) } %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) }
%a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
.tab-content.gitlab-tab-content .tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_member', tab_title: 'Add member' = render 'projects/project_members/new_project_member', tab_title: 'Invite member'
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
= render 'projects/project_members/new_project_group', tab_title: 'Invite group' = render 'projects/project_members/new_project_group', tab_title: 'Invite group'
- elsif !membership_locked? - elsif !membership_locked?
.invite-member= render 'projects/project_members/new_project_member', tab_title: 'Add member' .invite-member= render 'projects/project_members/new_project_member', tab_title: 'Invite member'
- elsif @project.allowed_to_share_with_group? - elsif @project.allowed_to_share_with_group?
.invite-group= render 'projects/project_members/new_project_group', tab_title: 'Invite group' .invite-group= render 'projects/project_members/new_project_group', tab_title: 'Invite group'
......
%h4.prepend-top-20 %h4.prepend-top-20
Contributions for = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) }
%strong= @calendar_date.to_s(:medium)
- if @events.any? - if @events.any?
%ul.bordered-list %ul.bordered-list
...@@ -9,6 +8,7 @@ ...@@ -9,6 +8,7 @@
%span.light %span.light
%i.fa.fa-clock-o %i.fa.fa-clock-o
= event.created_at.strftime('%-I:%M%P') = event.created_at.strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- if event.push? - if event.push?
#{event.action_name} #{event.ref_type} #{event.action_name} #{event.ref_type}
%strong %strong
...@@ -25,9 +25,11 @@ ...@@ -25,9 +25,11 @@
at at
%strong %strong
- if event.project - if event.project
= link_to_project event.project = link_to_project(event.project)
- else - else
= event.project_name = event.project_name
- else
made a private contribution
- else - else
%p %p
No contributions found for #{@calendar_date.to_s(:medium)} = _('No contributions were found')
...@@ -9,6 +9,8 @@ class NewMergeRequestWorker ...@@ -9,6 +9,8 @@ class NewMergeRequestWorker
EventCreateService.new.open_mr(issuable, user) EventCreateService.new.open_mr(issuable, user)
NotificationService.new.new_merge_request(issuable, user) NotificationService.new.new_merge_request(issuable, user)
issuable.diffs.write_cache
issuable.create_cross_references!(user) issuable.create_cross_references!(user)
end end
......
---
title: Allow gaps in multiseries metrics charts
merge_request: 21427
author:
type: fixed
---
title: Include private contributions to contributions calendar
merge_request: 17296
author: George Tsiolis
type: added
---
title: Fix NamespaceUploader.base_dir for remote uploads
merge_request:
author:
type: fixed
---
title: Replace white spaces in wiki attachments file names
merge_request: 21569
author:
type: fixed
---
title: Improved commit panel in Web IDE
merge_request: 21471
author:
type: changed
---
title: Added file templates to the Web IDE
merge_request:
author:
type: added
---
title: Move project services log to a separate file
merge_request:
author:
type: other
---
title: Send max_patch_bytes to Gitaly via Gitaly::CommitDiffRequest
merge_request: 21575
author:
type: other
---
title: Write diff highlighting cache upon MR creation (refactors caching)
merge_request: 21489
author:
type: performance
---
title: Remove orphaned label links
merge_request: 21552
author:
type: fixed
---
title: Administrative cleanup rake tasks now leverage Gitaly
merge_request: 21588
author:
type: changed
...@@ -213,4 +213,3 @@ ...@@ -213,4 +213,3 @@
label: Pod average label: Pod average
unit: "cores" unit: "cores"
track: canary track: canary
class AddIncludePrivateContributionsToUsers < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :users, :include_private_contributions, :boolean
end
end
# frozen_string_literal: true
class RemoveOrphanedLabelLinks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class LabelLinks < ActiveRecord::Base
self.table_name = 'label_links'
include EachBatch
def self.orphaned
where('NOT EXISTS ( SELECT 1 FROM labels WHERE labels.id = label_links.label_id )')
end
end
def up
# Some of these queries can take up to 10 seconds to run on GitLab.com,
# which is pretty close to our 15 second statement timeout. To ensure a
# smooth deployment procedure we disable the statement timeouts for this
# migration, just in case.
disable_statement_timeout do
# On GitLab.com there are over 2,000,000 orphaned label links. On
# staging, removing 100,000 rows generated a max replication lag of 6.7
# MB. In total, removing all these rows will only generate about 136 MB
# of data, so it should be safe to do this.
LabelLinks.orphaned.each_batch(of: 100_000) do |batch|
batch.delete_all
end
end
add_concurrent_foreign_key(:label_links, :labels, column: :label_id, on_delete: :cascade)
end
def down
# There is no way to restore orphaned label links.
if foreign_key_exists?(:label_links, column: :label_id)
remove_foreign_key(:label_links, column: :label_id)
end
end
end
...@@ -2892,6 +2892,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -2892,6 +2892,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do
t.string "feed_token" t.string "feed_token"
t.boolean "private_profile" t.boolean "private_profile"
t.integer "roadmap_layout", limit: 2 t.integer "roadmap_layout", limit: 2
t.boolean "include_private_contributions"
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
...@@ -3108,6 +3109,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -3108,6 +3109,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_links", "labels", name: "fk_d97dd08678", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
......
...@@ -113,6 +113,19 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was ...@@ -113,6 +113,19 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was
October 07, 2014 11:25: Project "project133" was removed October 07, 2014 11:25: Project "project133" was removed
``` ```
## `integrations_json.log`
This file lives in `/var/log/gitlab/gitlab-rails/integrations_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/integrations_json.log` for
installations from source.
It contains information about [integrations](../user/project/integrations/project_services.md) activities such as JIRA, Asana and Irker services. It uses JSON format like the example below:
``` json
{"severity":"ERROR","time":"2018-09-06T14:56:20.439Z","service_class":"JiraService","project_id":8,"project_path":"h5bp/html5-boilerplate","message":"Error sending message","client_url":"http://jira.gitlap.com:8080","error":"execution expired"}
{"severity":"INFO","time":"2018-09-06T17:15:16.365Z","service_class":"JiraService","project_id":3,"project_path":"namespace2/project2","message":"Successfully posted","client_url":"http://jira.example.net"}
```
## `githost.log` ## `githost.log`
This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
......
...@@ -6,8 +6,7 @@ We strive to support the 2-4 most important metrics for each common system servi ...@@ -6,8 +6,7 @@ We strive to support the 2-4 most important metrics for each common system servi
### Query identifier ### Query identifier
The requirement for adding a new metrics is to make each query to have an unique identifier. The requirement for adding a new metric is to make each query to have an unique identifier which is used to update the metric later when changed:
Identifier is used to update the metric later when changed.
```yaml ```yaml
- group: Response metrics (NGINX Ingress) - group: Response metrics (NGINX Ingress)
...@@ -25,9 +24,10 @@ Identifier is used to update the metric later when changed. ...@@ -25,9 +24,10 @@ Identifier is used to update the metric later when changed.
After you add or change existing _common_ metric you have to create a new database migration that will query and update all existing metrics. After you add or change existing _common_ metric you have to create a new database migration that will query and update all existing metrics.
**Note: If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.** NOTE: **Note:**
**You might want to add additional database migration that makes a decision what to do with removed one.** If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.
**For example: you might be interested in migrating all dependent data to a different metric.** You might want to add additional database migration that makes a decision what to do with removed one.
For example: you might be interested in migrating all dependent data to a different metric.
```ruby ```ruby
class ImportCommonMetrics < ActiveRecord::Migration class ImportCommonMetrics < ActiveRecord::Migration
......
...@@ -91,6 +91,18 @@ To enable private profile: ...@@ -91,6 +91,18 @@ To enable private profile:
NOTE: **Note:** NOTE: **Note:**
You and GitLab admins can see your the abovementioned information on your profile even if it is private. You and GitLab admins can see your the abovementioned information on your profile even if it is private.
## Private contributions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/14078) in GitLab 11.3.
Enabling private contributions will include contributions to private projects, in the user contribution calendar graph and user recent activity.
To enable private contributions:
1. Navigate to your personal [profile settings](#profile-settings).
2. Check the "Private contributions" option.
3. Hit **Update profile settings**.
## Current status ## Current status
> Introduced in GitLab 11.2. > Introduced in GitLab 11.2.
......
...@@ -78,13 +78,14 @@ switching to a different branch. ...@@ -78,13 +78,14 @@ switching to a different branch.
The Web IDE can be used to preview JavaScript projects right in the browser. The Web IDE can be used to preview JavaScript projects right in the browser.
This feature uses CodeSandbox to compile and bundle the JavaScript used to This feature uses CodeSandbox to compile and bundle the JavaScript used to
preview the web application. On public projects, an `Open in CodeSandbox` preview the web application.
button is visible which will transfer the contents of the project into a
CodeSandbox project to share with others.
**Note** this button is not visible on private or internal projects.
![Web IDE Client Side Evaluation](img/clientside_evaluation.png) ![Web IDE Client Side Evaluation](img/clientside_evaluation.png)
Additionally, for public projects an `Open in CodeSandbox` button is available
to transfer the contents of the project into a public CodeSandbox project to
quickly share your project with others.
### Enabling Client Side Evaluation ### Enabling Client Side Evaluation
The Client Side Evaluation feature needs to be enabled in the GitLab instances The Client Side Evaluation feature needs to be enabled in the GitLab instances
......
require 'spec_helper' require 'spec_helper'
describe 'Project > Members > Invite Group', :js do describe 'Project > Members > Invite group and members', :js do
include Select2Helper include Select2Helper
include ActionView::Helpers::DateHelper include ActionView::Helpers::DateHelper
let(:maintainer) { create(:user) } let(:maintainer) { create(:user) }
describe 'Invite group lock' do describe 'Share group lock' do
shared_examples 'the project cannot be shared with groups' do shared_examples 'the project cannot be shared with groups' do
it 'user is only able to share with members' do it 'user is only able to share with members' do
visit project_settings_members_path(project) visit project_settings_members_path(project)
...@@ -189,95 +189,4 @@ describe 'Project > Members > Invite Group', :js do ...@@ -189,95 +189,4 @@ describe 'Project > Members > Invite Group', :js do
end end
end end
end end
describe 'setting an expiration date for a group link' do
let(:project) { create(:project) }
let!(:group) { create(:group) }
around do |example|
Timecop.freeze { example.run }
end
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
visit project_settings_members_path(project)
click_on 'invite-group-tab'
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
click_on 'invite-group-tab'
find('.btn-create').click
end
it 'the group link shows the expiration time with a warning class' do
page.within('.project-members-groups') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
expect(page).to have_content(expires_in_text)
expect(page).to have_selector('.text-warning')
end
end
end
describe 'the groups dropdown' do
context 'with multiple groups to choose from' do
let(:project) { create(:project) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
create(:group).add_owner(maintainer)
create(:group).add_owner(maintainer)
visit project_settings_members_path(project)
click_link 'Invite group'
find('.ajax-groups-select.select2-container')
execute_script 'GROUP_SELECT_PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
context 'for a project in a nested group' do
let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:group_to_share_with) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
group.add_maintainer(maintainer)
group_to_share_with.add_maintainer(maintainer)
end
it 'the groups dropdown does not show ancestors', :nested_groups do
visit project_settings_members_path(project)
click_on 'invite-group-tab'
click_link 'Search for a group'
page.within '.select2-drop' do
expect(page).to have_content(group_to_share_with.name)
expect(page).not_to have_content(group.name)
end
end
end
end
end end
...@@ -8,8 +8,9 @@ module Banzai ...@@ -8,8 +8,9 @@ module Banzai
# #
# Based on Banzai::Filter::AutolinkFilter # Based on Banzai::Filter::AutolinkFilter
# #
# CommonMark does not allow spaces in the url portion of a link. # CommonMark does not allow spaces in the url portion of a link/url.
# For example, `[example](page slug)` is not valid. However, # For example, `[example](page slug)` is not valid.
# Neither is `![example](test image.jpg)`. However,
# in our wikis, we support (via RedCarpet) this type of link, allowing # in our wikis, we support (via RedCarpet) this type of link, allowing
# wiki pages to be easily linked by their title. This filter adds that functionality. # wiki pages to be easily linked by their title. This filter adds that functionality.
# The intent is for this to only be used in Wikis - in general, we want # The intent is for this to only be used in Wikis - in general, we want
...@@ -20,10 +21,17 @@ module Banzai ...@@ -20,10 +21,17 @@ module Banzai
# Pattern to match a standard markdown link # Pattern to match a standard markdown link
# #
# Rubular: http://rubular.com/r/z9EAHxYmKI # Rubular: http://rubular.com/r/2EXEQ49rg5
LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/ LINK_OR_IMAGE_PATTERN = %r{
(?<preview_operator>!)?
# Text matching LINK_PATTERN inside these elements will not be linked \[(?<text>.+?)\]
\(
(?<new_link>.+?)
(?<title>\ ".+?")?
\)
}x
# Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set IGNORE_PARENTS = %w(a code kbd pre script style).to_set
# The XPath query to use for finding text nodes to parse. # The XPath query to use for finding text nodes to parse.
...@@ -38,7 +46,7 @@ module Banzai ...@@ -38,7 +46,7 @@ module Banzai
doc.xpath(TEXT_QUERY).each do |node| doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html content = node.to_html
next unless content.match(LINK_PATTERN) next unless content.match(LINK_OR_IMAGE_PATTERN)
html = spaced_link_filter(content) html = spaced_link_filter(content)
...@@ -53,25 +61,37 @@ module Banzai ...@@ -53,25 +61,37 @@ module Banzai
private private
def spaced_link_match(link) def spaced_link_match(link)
match = LINK_PATTERN.match(link) match = LINK_OR_IMAGE_PATTERN.match(link)
return link unless match && match[1] && match[2] return link unless match
# escape the spaces in the url so that it's a valid markdown link, # escape the spaces in the url so that it's a valid markdown link,
# then run it through the markdown processor again, let it do its magic # then run it through the markdown processor again, let it do its magic
text = match[1] html = Banzai::Filter::MarkdownFilter.call(transform_markdown(match), context)
new_link = match[2].gsub(' ', '%20')
title = match[3] ? " \"#{match[3]}\"" : ''
html = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context)
# link is wrapped in a <p>, so strip that off # link is wrapped in a <p>, so strip that off
html.sub('<p>', '').chomp('</p>') html.sub('<p>', '').chomp('</p>')
end end
def spaced_link_filter(text) def spaced_link_filter(text)
Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:| Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_OR_IMAGE_PATTERN) do |link, left:, right:|
spaced_link_match(link) spaced_link_match(link)
end end
end end
def transform_markdown(match)
preview_operator, text, new_link, title = process_match(match)
"#{preview_operator}[#{text}](#{new_link}#{title})"
end
def process_match(match)
[
match[:preview_operator],
match[:text],
match[:new_link].gsub(' ', '%20'),
match[:title]
]
end
end end
end end
end end
...@@ -5,7 +5,7 @@ module Banzai ...@@ -5,7 +5,7 @@ module Banzai
@filters ||= begin @filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
.insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter) .insert_before(Filter::VideoLinkFilter, Filter::SpacedLinkFilter)
end end
end end
end end
......
...@@ -7,7 +7,11 @@ module Gitlab ...@@ -7,7 +7,11 @@ module Gitlab
def initialize(contributor, current_user = nil) def initialize(contributor, current_user = nil)
@contributor = contributor @contributor = contributor
@current_user = current_user @current_user = current_user
@projects = ContributedProjectsFinder.new(contributor).execute(current_user) @projects = if @contributor.include_private_contributions?
ContributedProjectsFinder.new(@contributor).execute(@contributor)
else
ContributedProjectsFinder.new(contributor).execute(current_user)
end
end end
def activity_dates def activity_dates
...@@ -36,13 +40,9 @@ module Gitlab ...@@ -36,13 +40,9 @@ module Gitlab
def events_by_date(date) def events_by_date(date)
return Event.none unless can_read_cross_project? return Event.none unless can_read_cross_project?
events = Event.contributions.where(author_id: contributor.id) Event.contributions.where(author_id: contributor.id)
.where(created_at: date.beginning_of_day..date.end_of_day) .where(created_at: date.beginning_of_day..date.end_of_day)
.where(project_id: projects) .where(project_id: projects)
# Use visible_to_user? instead of the complicated logic in activity_dates
# because we're only viewing the events for a single day.
events.select { |event| event.visible_to_user?(current_user) }
end end
def starting_year def starting_year
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Diff module Diff
module FileCollection module FileCollection
class Base class Base
attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs, :diffable
delegate :count, :size, :real_size, to: :diff_files delegate :count, :size, :real_size, to: :diff_files
...@@ -33,6 +33,14 @@ module Gitlab ...@@ -33,6 +33,14 @@ module Gitlab
diff_files.find { |diff_file| diff_file.new_path == new_path } diff_files.find { |diff_file| diff_file.new_path == new_path }
end end
def clear_cache
# No-op
end
def write_cache
# No-op
end
private private
def decorate_diff!(diff) def decorate_diff!(diff)
......
...@@ -2,6 +2,8 @@ module Gitlab ...@@ -2,6 +2,8 @@ module Gitlab
module Diff module Diff
module FileCollection module FileCollection
class MergeRequestDiff < Base class MergeRequestDiff < Base
extend ::Gitlab::Utils::Override
def initialize(merge_request_diff, diff_options:) def initialize(merge_request_diff, diff_options:)
@merge_request_diff = merge_request_diff @merge_request_diff = merge_request_diff
...@@ -13,70 +15,35 @@ module Gitlab ...@@ -13,70 +15,35 @@ module Gitlab
end end
def diff_files def diff_files
# Make sure to _not_ send any method call to Gitlab::Diff::File
# _before_ all of them were collected (`super`). Premature method calls will
# trigger N+1 RPCs to Gitaly through BatchLoader records (Blob.lazy).
#
diff_files = super diff_files = super
diff_files.each { |diff_file| cache_highlight!(diff_file) if cacheable?(diff_file) } diff_files.each { |diff_file| cache.decorate(diff_file) }
store_highlight_cache
diff_files diff_files
end end
def real_size override :write_cache
@merge_request_diff.real_size def write_cache
cache.write_if_empty
end end
def clear_cache! override :clear_cache
Rails.cache.delete(cache_key) def clear_cache
cache.clear
end end
def cache_key def cache_key
[@merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options] cache.key
end end
private def real_size
@merge_request_diff.real_size
def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
diff_file.highlighted_diff_lines = cache_diff_lines.map do |line|
Gitlab::Diff::Line.init_from_hash(line)
end
end
#
# If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted)
# for the highlighted ones, so we just skip their execution.
# If the highlighted diff files lines are not cached we calculate and cache them.
#
# The content of the cache is a Hash where the key identifies the file and the values are Arrays of
# hashes that represent serialized diff lines.
#
def cache_highlight!(diff_file)
item_key = diff_file.file_identifier
if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
else
highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash)
end
end
def highlight_cache
return @highlight_cache if defined?(@highlight_cache)
@highlight_cache = Rails.cache.read(cache_key) || {}
@highlight_cache_was_empty = @highlight_cache.empty?
@highlight_cache
end end
def store_highlight_cache private
Rails.cache.write(cache_key, highlight_cache, expires_in: 1.week) if @highlight_cache_was_empty
end
def cacheable?(diff_file) def cache
@merge_request_diff.present? && diff_file.text? && diff_file.diffable? @cache ||= Gitlab::Diff::HighlightCache.new(self)
end end
end end
end end
......
# frozen_string_literal: true
#
module Gitlab
module Diff
class HighlightCache
delegate :diffable, to: :@diff_collection
delegate :diff_options, to: :@diff_collection
def initialize(diff_collection, backend: Rails.cache)
@backend = backend
@diff_collection = diff_collection
end
# - Reads from cache
# - Assigns DiffFile#highlighted_diff_lines for cached files
def decorate(diff_file)
if content = read_file(diff_file)
diff_file.highlighted_diff_lines = content.map do |line|
Gitlab::Diff::Line.init_from_hash(line)
end
end
end
# It populates a Hash in order to submit a single write to the memory
# cache. This avoids excessive IO generated by N+1's (1 writing for
# each highlighted line or file).
def write_if_empty
return if cached_content.present?
@diff_collection.diff_files.each do |diff_file|
next unless cacheable?(diff_file)
diff_file_id = diff_file.file_identifier
cached_content[diff_file_id] = diff_file.highlighted_diff_lines.map(&:to_hash)
end
cache.write(key, cached_content, expires_in: 1.week)
end
def clear
cache.delete(key)
end
def key
[diffable, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options]
end
private
def read_file(diff_file)
cached_content[diff_file.file_identifier]
end
def cache
@backend
end
def cached_content
@cached_content ||= cache.read(key) || {}
end
def cacheable?(diff_file)
diffable.present? && diff_file.text? && diff_file.diffable?
end
end
end
end
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits
def self.collection_limits(options = {}) def self.limits(options = {})
limits = {} limits = {}
limits[:max_files] = options.fetch(:max_files, DEFAULT_LIMITS[:max_files]) limits[:max_files] = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
limits[:max_lines] = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines]) limits[:max_lines] = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
...@@ -19,13 +19,14 @@ module Gitlab ...@@ -19,13 +19,14 @@ module Gitlab
limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min
limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min
limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file
limits[:max_patch_bytes] = Gitlab::Git::Diff::SIZE_LIMIT
OpenStruct.new(limits) OpenStruct.new(limits)
end end
def initialize(iterator, options = {}) def initialize(iterator, options = {})
@iterator = iterator @iterator = iterator
@limits = self.class.collection_limits(options) @limits = self.class.limits(options)
@enforce_limits = !!options.fetch(:limits, true) @enforce_limits = !!options.fetch(:limits, true)
@expanded = !!options.fetch(:expanded, true) @expanded = !!options.fetch(:expanded, true)
......
...@@ -369,7 +369,7 @@ module Gitlab ...@@ -369,7 +369,7 @@ module Gitlab
request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
request_params[:enforce_limits] = options.fetch(:limits, true) request_params[:enforce_limits] = options.fetch(:limits, true)
request_params[:collapse_diffs] = !options.fetch(:expanded, true) request_params[:collapse_diffs] = !options.fetch(:expanded, true)
request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h) request_params.merge!(Gitlab::Git::DiffCollection.limits(options).to_h)
request = Gitaly::CommitDiffRequest.new(request_params) request = Gitaly::CommitDiffRequest.new(request_params)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout) response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout)
......
module Gitlab module Gitlab
module GitalyClient module GitalyClient
class RemoteService class RemoteService
include Gitlab::EncodingHelper
MAX_MSG_SIZE = 128.kilobytes.freeze MAX_MSG_SIZE = 128.kilobytes.freeze
def self.exists?(remote_url) def self.exists?(remote_url)
...@@ -61,7 +63,7 @@ module Gitlab ...@@ -61,7 +63,7 @@ module Gitlab
response = GitalyClient.call(@storage, :remote_service, response = GitalyClient.call(@storage, :remote_service,
:find_remote_root_ref, request) :find_remote_root_ref, request)
response.ref.presence encode_utf8(response.ref)
end end
def update_remote_mirror(ref_name, only_branches_matching) def update_remote_mirror(ref_name, only_branches_matching)
......
...@@ -5,6 +5,14 @@ module Gitlab ...@@ -5,6 +5,14 @@ module Gitlab
@storage = storage @storage = storage
end end
# Returns all directories in the git storage directory, lexically ordered
def list_directories(depth: 1)
request = Gitaly::ListDirectoriesRequest.new(storage_name: @storage, depth: depth)
GitalyClient.call(@storage, :storage_service, :list_directories, request)
.flat_map(&:paths)
end
# Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation. # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation.
def delete_all_repositories def delete_all_repositories
request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage) request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage)
......
module Gitlab
class ProjectServiceLogger < Gitlab::JsonLogger
def self.file_name_noext
'integrations_json'
end
end
end
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/954 # frozen_string_literal: true
# require 'set'
namespace :gitlab do namespace :gitlab do
namespace :cleanup do namespace :cleanup do
HASHED_REPOSITORY_NAME = '@hashed'.freeze
desc "GitLab | Cleanup | Clean namespaces" desc "GitLab | Cleanup | Clean namespaces"
task dirs: :gitlab_environment do task dirs: :gitlab_environment do
warn_user_is_not_gitlab namespaces = Set.new(Namespace.pluck(:path))
namespaces << Storage::HashedProject::ROOT_PATH_PREFIX
namespaces = Namespace.pluck(:path) Gitaly::Server.all.each do |server|
namespaces << HASHED_REPOSITORY_NAME # add so that it will be ignored all_dirs = Gitlab::GitalyClient::StorageService
Gitlab.config.repositories.storages.each do |name, repository_storage| .new(server.storage)
git_base_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path } .list_directories(depth: 0)
all_dirs = Dir.glob(git_base_path + '/*') .reject { |dir| dir.ends_with?('.git') || namespaces.include?(File.basename(dir)) }
puts git_base_path.color(:yellow)
puts "Looking for directories to remove... " puts "Looking for directories to remove... "
all_dirs.reject! do |dir|
# skip if git repo
dir =~ /.git$/
end
all_dirs.reject! do |dir|
dir_name = File.basename dir
# skip if namespace present
namespaces.include?(dir_name)
end
all_dirs.each do |dir_path| all_dirs.each do |dir_path|
if remove? if remove?
if FileUtils.rm_rf dir_path begin
puts "Removed...#{dir_path}".color(:red) Gitlab::GitalyClient::NamespaceService.new(server.storage)
else .remove(dir_path)
puts "Cannot remove #{dir_path}".color(:red)
puts "Removed...#{dir_path}"
rescue StandardError => e
puts "Cannot remove #{dir_path}: #{e.message}".color(:red)
end end
else else
puts "Can be removed: #{dir_path}".color(:red) puts "Can be removed: #{dir_path}".color(:red)
...@@ -79,29 +68,29 @@ namespace :gitlab do ...@@ -79,29 +68,29 @@ namespace :gitlab do
desc "GitLab | Cleanup | Clean repositories" desc "GitLab | Cleanup | Clean repositories"
task repos: :gitlab_environment do task repos: :gitlab_environment do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}" move_suffix = "+orphaned+#{Time.now.to_i}"
Gitlab.config.repositories.storages.each do |name, repository_storage|
repo_root = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path }
# Look for global repos (legacy, depth 1) and normal repos (depth 2) Gitaly::Server.all.each do |server|
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| Gitlab::GitalyClient::StorageService
find.each_line do |path| .new(server.storage)
path.chomp! .list_directories
repo_with_namespace = path .each do |path|
.sub(repo_root, '') repo_with_namespace = path.chomp('.git').chomp('.wiki')
.sub(%r{^/*}, '')
.chomp('.git')
.chomp('.wiki')
# TODO ignoring hashed repositories for now. But revisit to fully support # TODO ignoring hashed repositories for now. But revisit to fully support
# possible orphaned hashed repos # possible orphaned hashed repos
next if repo_with_namespace.start_with?("#{HASHED_REPOSITORY_NAME}/") || Project.find_by_full_path(repo_with_namespace) next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX)
next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
begin
Gitlab::GitalyClient::NamespaceService
.new(server.storage)
.rename(path, new_path)
rescue StandardError => e
puts "Error occured while moving the repository: #{e.message}".color(:red)
end end
end end
end end
......
...@@ -1421,6 +1421,12 @@ msgstr "" ...@@ -1421,6 +1421,12 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr "" msgstr ""
msgid "Choose a template..."
msgstr ""
msgid "Choose a type..."
msgstr ""
msgid "Choose any color." msgid "Choose any color."
msgstr "" msgstr ""
...@@ -2202,6 +2208,9 @@ msgstr "" ...@@ -2202,6 +2208,9 @@ msgstr ""
msgid "Contribution guide" msgid "Contribution guide"
msgstr "" msgstr ""
msgid "Contributions for <strong>%{calendar_date}</strong>"
msgstr ""
msgid "Contributions per group member" msgid "Contributions per group member"
msgstr "" msgstr ""
...@@ -2672,9 +2681,21 @@ msgstr "" ...@@ -2672,9 +2681,21 @@ msgstr ""
msgid "Disable group Runners" msgid "Disable group Runners"
msgstr "" msgstr ""
msgid "Discard"
msgstr ""
msgid "Discard all changes"
msgstr ""
msgid "Discard all unstaged changes?"
msgstr ""
msgid "Discard changes" msgid "Discard changes"
msgstr "" msgstr ""
msgid "Discard changes to %{path}?"
msgstr ""
msgid "Discard draft" msgid "Discard draft"
msgstr "" msgstr ""
...@@ -3173,6 +3194,9 @@ msgstr "" ...@@ -3173,6 +3194,9 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure" msgid "Fields on this page are now uneditable, you can configure"
msgstr "" msgstr ""
msgid "File templates"
msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
...@@ -3188,6 +3212,9 @@ msgstr "" ...@@ -3188,6 +3212,9 @@ msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
msgstr "" msgstr ""
msgid "Filter..."
msgstr ""
msgid "Find by path" msgid "Find by path"
msgstr "" msgstr ""
...@@ -4825,9 +4852,6 @@ msgstr "" ...@@ -4825,9 +4852,6 @@ msgstr ""
msgid "More" msgid "More"
msgstr "" msgstr ""
msgid "More actions"
msgstr ""
msgid "More info" msgid "More info"
msgstr "" msgstr ""
...@@ -4980,6 +5004,9 @@ msgstr "" ...@@ -4980,6 +5004,9 @@ msgstr ""
msgid "No container images stored for this project. Add one by following the instructions above." msgid "No container images stored for this project. Add one by following the instructions above."
msgstr "" msgstr ""
msgid "No contributions were found"
msgstr ""
msgid "No due date" msgid "No due date"
msgstr "" msgstr ""
...@@ -5588,6 +5615,9 @@ msgstr "" ...@@ -5588,6 +5615,9 @@ msgstr ""
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
msgstr "" msgstr ""
msgid "Profiles|%{author_name} made a private contribution"
msgstr ""
msgid "Profiles|Account scheduled for removal." msgid "Profiles|Account scheduled for removal."
msgstr "" msgstr ""
...@@ -5597,15 +5627,30 @@ msgstr "" ...@@ -5597,15 +5627,30 @@ msgstr ""
msgid "Profiles|Add status emoji" msgid "Profiles|Add status emoji"
msgstr "" msgstr ""
msgid "Profiles|Avatar cropper"
msgstr ""
msgid "Profiles|Avatar will be removed. Are you sure?"
msgstr ""
msgid "Profiles|Change username" msgid "Profiles|Change username"
msgstr "" msgstr ""
msgid "Profiles|Choose file..."
msgstr ""
msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information."
msgstr ""
msgid "Profiles|Clear status" msgid "Profiles|Clear status"
msgstr "" msgstr ""
msgid "Profiles|Current path: %{path}" msgid "Profiles|Current path: %{path}"
msgstr "" msgstr ""
msgid "Profiles|Current status"
msgstr ""
msgid "Profiles|Delete Account" msgid "Profiles|Delete Account"
msgstr "" msgstr ""
...@@ -5618,39 +5663,108 @@ msgstr "" ...@@ -5618,39 +5663,108 @@ msgstr ""
msgid "Profiles|Deleting an account has the following effects:" msgid "Profiles|Deleting an account has the following effects:"
msgstr "" msgstr ""
msgid "Profiles|Do not show on profile"
msgstr ""
msgid "Profiles|Don't display activity-related personal information on your profiles"
msgstr ""
msgid "Profiles|Edit Profile"
msgstr ""
msgid "Profiles|Invalid password" msgid "Profiles|Invalid password"
msgstr "" msgstr ""
msgid "Profiles|Invalid username" msgid "Profiles|Invalid username"
msgstr "" msgstr ""
msgid "Profiles|Main settings"
msgstr ""
msgid "Profiles|No file chosen"
msgstr ""
msgid "Profiles|Path" msgid "Profiles|Path"
msgstr "" msgstr ""
msgid "Profiles|Position and size your new avatar"
msgstr ""
msgid "Profiles|Private contributions"
msgstr ""
msgid "Profiles|Public Avatar"
msgstr ""
msgid "Profiles|Remove avatar"
msgstr ""
msgid "Profiles|Set new profile picture"
msgstr ""
msgid "Profiles|Some options are unavailable for LDAP accounts"
msgstr ""
msgid "Profiles|Tell us about yourself in fewer than 250 characters."
msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?" msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr "" msgstr ""
msgid "Profiles|This email will be displayed on your public profile."
msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr "" msgstr ""
msgid "Profiles|This feature is experimental and translations are not complete yet."
msgstr ""
msgid "Profiles|This information will appear on your profile."
msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:" msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "" msgstr ""
msgid "Profiles|Typically starts with \"ssh-rsa …\"" msgid "Profiles|Typically starts with \"ssh-rsa …\""
msgstr "" msgstr ""
msgid "Profiles|Update profile settings"
msgstr ""
msgid "Profiles|Update username" msgid "Profiles|Update username"
msgstr "" msgstr ""
msgid "Profiles|Upload new avatar"
msgstr ""
msgid "Profiles|Username change failed - %{message}" msgid "Profiles|Username change failed - %{message}"
msgstr "" msgstr ""
msgid "Profiles|Username successfully changed" msgid "Profiles|Username successfully changed"
msgstr "" msgstr ""
msgid "Profiles|Website"
msgstr ""
msgid "Profiles|What's your status?" msgid "Profiles|What's your status?"
msgstr "" msgstr ""
msgid "Profiles|You can change your avatar here"
msgstr ""
msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}"
msgstr ""
msgid "Profiles|You can upload your avatar here"
msgstr ""
msgid "Profiles|You can upload your avatar here or change it at %{gravatar_link}"
msgstr ""
msgid "Profiles|You don't have access to delete this user." msgid "Profiles|You don't have access to delete this user."
msgstr "" msgstr ""
...@@ -5660,6 +5774,15 @@ msgstr "" ...@@ -5660,6 +5774,15 @@ msgstr ""
msgid "Profiles|Your account is currently an owner in these groups:" msgid "Profiles|Your account is currently an owner in these groups:"
msgstr "" msgstr ""
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account."
msgstr ""
msgid "Profiles|Your location was automatically set based on your %{provider_label} account."
msgstr ""
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
msgstr ""
msgid "Profiles|Your status" msgid "Profiles|Your status"
msgstr "" msgstr ""
...@@ -7266,6 +7389,12 @@ msgstr "" ...@@ -7266,6 +7389,12 @@ msgstr ""
msgid "There are no projects shared with this group yet" msgid "There are no projects shared with this group yet"
msgstr "" msgstr ""
msgid "There are no staged changes"
msgstr ""
msgid "There are no unstaged changes"
msgstr ""
msgid "There are problems accessing Git storage: " msgid "There are problems accessing Git storage: "
msgstr "" msgstr ""
...@@ -7786,6 +7915,9 @@ msgstr "" ...@@ -7786,6 +7915,9 @@ msgstr ""
msgid "Unable to sign you in to the group with SAML due to \"%{reason}\"" msgid "Unable to sign you in to the group with SAML due to \"%{reason}\""
msgstr "" msgstr ""
msgid "Undo"
msgstr ""
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
...@@ -7801,6 +7933,9 @@ msgstr "" ...@@ -7801,6 +7933,9 @@ msgstr ""
msgid "Unresolve discussion" msgid "Unresolve discussion"
msgstr "" msgstr ""
msgid "Unstage"
msgstr ""
msgid "Unstage all changes" msgid "Unstage all changes"
msgstr "" msgstr ""
...@@ -7870,9 +8005,6 @@ msgstr "" ...@@ -7870,9 +8005,6 @@ msgstr ""
msgid "Upload file" msgid "Upload file"
msgstr "" msgstr ""
msgid "Upload new avatar"
msgstr ""
msgid "UploadLink|click to upload" msgid "UploadLink|click to upload"
msgstr "" msgstr ""
...@@ -7924,9 +8056,6 @@ msgstr "" ...@@ -7924,9 +8056,6 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
msgid "User|Current status"
msgstr ""
msgid "Variables" msgid "Variables"
msgstr "" msgstr ""
...@@ -8284,6 +8413,12 @@ msgstr "" ...@@ -8284,6 +8413,12 @@ msgstr ""
msgid "You need permission." msgid "You need permission."
msgstr "" msgstr ""
msgid "You will loose all changes you've made to this file. This action cannot be undone."
msgstr ""
msgid "You will loose all the unstaged changes you've made in this project. This action cannot be undone."
msgstr ""
msgid "You will not get any notifications via email" msgid "You will not get any notifications via email"
msgstr "" msgstr ""
......
...@@ -21,6 +21,8 @@ Disallow: /groups/new ...@@ -21,6 +21,8 @@ Disallow: /groups/new
Disallow: /groups/*/edit Disallow: /groups/*/edit
Disallow: /users Disallow: /users
Disallow: /help Disallow: /help
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in
# Global snippets # Global snippets
User-Agent: * User-Agent: *
......
...@@ -100,7 +100,7 @@ module QA ...@@ -100,7 +100,7 @@ module QA
end end
module Sanity module Sanity
autoload :Failing, 'qa/scenario/test/sanity/failing' autoload :Framework, 'qa/scenario/test/sanity/framework'
autoload :Selectors, 'qa/scenario/test/sanity/selectors' autoload :Selectors, 'qa/scenario/test/sanity/selectors'
end end
end end
......
...@@ -63,6 +63,14 @@ module QA ...@@ -63,6 +63,14 @@ module QA
'/users/sign_in' '/users/sign_in'
end end
def sign_in_tab?
page.has_button?('Sign in')
end
def ldap_tab?
page.has_button?('LDAP')
end
def switch_to_sign_in_tab def switch_to_sign_in_tab
click_on 'Sign in' click_on 'Sign in'
end end
...@@ -90,8 +98,8 @@ module QA ...@@ -90,8 +98,8 @@ module QA
end end
def sign_in_using_gitlab_credentials(user) def sign_in_using_gitlab_credentials(user)
switch_to_sign_in_tab unless page.has_button?('Sign in') switch_to_sign_in_tab unless sign_in_tab?
switch_to_standard_tab if page.has_content?('LDAP') switch_to_standard_tab if ldap_tab?
fill_in :user_login, with: user.username fill_in :user_login, with: user.username
fill_in :user_password, with: user.password fill_in :user_password, with: user.password
......
...@@ -5,12 +5,13 @@ module QA ...@@ -5,12 +5,13 @@ module QA
module Test module Test
module Sanity module Sanity
## ##
# This scenario exits with a 1 exit code. # This scenario runs 1 passing example, and 1 failing example, and exits
# with a 1 exit code.
# #
class Failing < Template class Framework < Template
include Bootable include Bootable
tags :failing tags :framework
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
context 'Sanity checks', :orchestrated, :failing do context 'Framework sanity checks', :orchestrated, :framework do
describe 'Passing orchestrated example' do
it 'succeeds' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform do |main_login|
expect(main_login.sign_in_tab?).to be(true)
end
end
end
describe 'Failing orchestrated example' do describe 'Failing orchestrated example' do
it 'always fails' do it 'fails' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
expect(page).to have_text("These Aren't the Texts You're Looking For", wait: 1) expect(page).to have_text("These Aren't the Texts You're Looking For", wait: 1)
......
describe QA::Scenario::Test::Sanity::Framework do
it_behaves_like 'a QA scenario class' do
let(:tags) { [:framework] }
end
end
...@@ -95,6 +95,7 @@ describe 'bin/changelog' do ...@@ -95,6 +95,7 @@ describe 'bin/changelog' do
it 'shows error message and exits the program' do it 'shows error message and exits the program' do
allow($stdin).to receive(:getc).and_return(type) allow($stdin).to receive(:getc).and_return(type)
expect do expect do
expect { described_class.read_type }.to raise_error( expect { described_class.read_type }.to raise_error(
ChangelogHelpers::Abort, ChangelogHelpers::Abort,
......
...@@ -166,8 +166,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -166,8 +166,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/job_details') expect(response).to match_response_schema('job/job_details')
expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) expect(json_response['artifact']['download_path']).to match(%r{artifacts/download})
expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse})
expect(json_response['artifact']).not_to have_key(:expired) expect(json_response['artifact']).not_to have_key('expired')
expect(json_response['artifact']).not_to have_key(:expired_at) expect(json_response['artifact']).not_to have_key('expired_at')
end end
end end
...@@ -177,8 +177,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -177,8 +177,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'exposes needed information' do it 'exposes needed information' do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details') expect(response).to match_response_schema('job/job_details')
expect(json_response['artifact']).not_to have_key(:download_path) expect(json_response['artifact']).not_to have_key('download_path')
expect(json_response['artifact']).not_to have_key(:browse_path) expect(json_response['artifact']).not_to have_key('browse_path')
expect(json_response['artifact']['expired']).to eq(true) expect(json_response['artifact']['expired']).to eq(true)
expect(json_response['artifact']['expire_at']).not_to be_empty expect(json_response['artifact']['expire_at']).not_to be_empty
end end
......
require 'spec_helper'
describe 'Project > Members > Invite group', :js do
include Select2Helper
include ActionView::Helpers::DateHelper
let(:maintainer) { create(:user) }
describe 'Share with group lock' do
shared_examples 'the project can be shared with groups' do
it 'the "Invite group" tab exists' do
visit project_settings_members_path(project)
expect(page).to have_selector('#invite-group-tab')
end
end
shared_examples 'the project cannot be shared with groups' do
it 'the "Invite group" tab does not exist' do
visit project_settings_members_path(project)
expect(page).not_to have_selector('#invite-group-tab')
end
end
context 'for a project in a root group' do
let!(:group_to_share_with) { create(:group) }
let(:project) { create(:project, namespace: create(:group)) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
end
context 'when the group has "Share group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
it 'the project can be shared with another group' do
visit project_settings_members_path(project)
click_on 'invite-group-tab'
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
find('.btn-create').click
page.within('.project-members-groups') do
expect(page).to have_content(group_to_share_with.name)
end
end
end
context 'when the group has "Share group lock" enabled' do
before do
project.namespace.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
context 'for a project in a subgroup', :nested_groups do
let!(:group_to_share_with) { create(:group) }
let(:root_group) { create(:group) }
let(:subgroup) { create(:group, parent: root_group) }
let(:project) { create(:project, namespace: subgroup) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
end
context 'when the root_group has "Share group lock" disabled' do
context 'when the subgroup has "Share group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
end
context 'when the subgroup has "Share group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
context 'when the root_group has "Share group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share group lock" disabled (parent overridden)' do
it_behaves_like 'the project can be shared with groups'
end
context 'when the subgroup has "Share group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
it_behaves_like 'the project cannot be shared with groups'
end
end
end
end
describe 'setting an expiration date for a group link' do
let(:project) { create(:project) }
let!(:group) { create(:group) }
around do |example|
Timecop.freeze { example.run }
end
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
visit project_settings_members_path(project)
click_on 'invite-group-tab'
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
click_on 'invite-group-tab'
find('.btn-create').click
end
it 'the group link shows the expiration time with a warning class' do
page.within('.project-members-groups') do
# Using distance_of_time_in_words_to_now because it is not the same as
# subtraction, and this way avoids time zone issues as well
expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
expect(page).to have_content(expires_in_text)
expect(page).to have_selector('.text-warning')
end
end
end
describe 'the groups dropdown' do
context 'with multiple groups to choose from' do
let(:project) { create(:project) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
create(:group).add_owner(maintainer)
create(:group).add_owner(maintainer)
visit project_settings_members_path(project)
click_link 'Invite group'
find('.ajax-groups-select.select2-container')
execute_script 'GROUP_SELECT_PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
context 'for a project in a nested group' do
let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:group_to_share_with) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
before do
project.add_maintainer(maintainer)
sign_in(maintainer)
group.add_maintainer(maintainer)
group_to_share_with.add_maintainer(maintainer)
end
it 'the groups dropdown does not show ancestors', :nested_groups do
visit project_settings_members_path(project)
click_on 'invite-group-tab'
click_link 'Search for a group'
page.within '.select2-drop' do
expect(page).to have_content(group_to_share_with.name)
expect(page).not_to have_content(group.name)
end
end
end
end
end
...@@ -8,6 +8,7 @@ describe ContributedProjectsFinder do ...@@ -8,6 +8,7 @@ describe ContributedProjectsFinder do
let!(:public_project) { create(:project, :public) } let!(:public_project) { create(:project, :public) }
let!(:private_project) { create(:project, :private) } let!(:private_project) { create(:project, :private) }
let!(:internal_project) { create(:project, :internal) }
before do before do
private_project.add_maintainer(source_user) private_project.add_maintainer(source_user)
...@@ -16,17 +17,18 @@ describe ContributedProjectsFinder do ...@@ -16,17 +17,18 @@ describe ContributedProjectsFinder do
create(:push_event, project: public_project, author: source_user) create(:push_event, project: public_project, author: source_user)
create(:push_event, project: private_project, author: source_user) create(:push_event, project: private_project, author: source_user)
create(:push_event, project: internal_project, author: source_user)
end end
describe 'without a current user' do describe 'activity without a current user' do
subject { finder.execute } subject { finder.execute }
it { is_expected.to eq([public_project]) } it { is_expected.to match_array([public_project]) }
end end
describe 'with a current user' do describe 'activity with a current user' do
subject { finder.execute(current_user) } subject { finder.execute(current_user) }
it { is_expected.to eq([private_project, public_project]) } it { is_expected.to match_array([private_project, internal_project, public_project]) }
end end
end end
...@@ -13,22 +13,6 @@ describe UserRecentEventsFinder do ...@@ -13,22 +13,6 @@ describe UserRecentEventsFinder do
subject(:finder) { described_class.new(current_user, project_owner) } subject(:finder) { described_class.new(current_user, project_owner) }
describe '#execute' do describe '#execute' do
context 'current user does not have access to projects' do
it 'returns public and internal events' do
records = finder.execute
expect(records).to include(public_event, internal_event)
expect(records).not_to include(private_event)
end
end
context 'when current user has access to the projects' do
before do
private_project.add_developer(current_user)
internal_project.add_developer(current_user)
public_project.add_developer(current_user)
end
context 'when profile is public' do context 'when profile is public' do
it 'returns all the events' do it 'returns all the events' do
expect(finder.execute).to include(private_event, internal_event, public_event) expect(finder.execute).to include(private_event, internal_event, public_event)
...@@ -39,6 +23,7 @@ describe UserRecentEventsFinder do ...@@ -39,6 +23,7 @@ describe UserRecentEventsFinder do
it 'returns no event' do it 'returns no event' do
allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false) allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false)
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
end end
...@@ -49,13 +34,4 @@ describe UserRecentEventsFinder do ...@@ -49,13 +34,4 @@ describe UserRecentEventsFinder do
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
end end
context 'when current user is anonymous' do
let(:current_user) { nil }
it 'returns public events only' do
expect(finder.execute).to eq([public_event])
end
end
end
end end
...@@ -30,7 +30,7 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -30,7 +30,7 @@ describe('Multi-file editor commit sidebar list item', () => {
}); });
it('renders file path', () => { it('renders file path', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path);
}); });
it('renders actionn button', () => { it('renders actionn button', () => {
......
...@@ -29,7 +29,7 @@ describe('IDE stage file button', () => { ...@@ -29,7 +29,7 @@ describe('IDE stage file button', () => {
}); });
it('renders button to discard & stage', () => { it('renders button to discard & stage', () => {
expect(vm.$el.querySelectorAll('.btn').length).toBe(2); expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2);
}); });
it('calls store with stage button', () => { it('calls store with stage button', () => {
...@@ -39,7 +39,7 @@ describe('IDE stage file button', () => { ...@@ -39,7 +39,7 @@ describe('IDE stage file button', () => {
}); });
it('calls store with discard button', () => { it('calls store with discard button', () => {
vm.$el.querySelector('.dropdown-menu button').click(); vm.$el.querySelector('.btn-danger').click();
expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
}); });
......
import Vue from 'vue';
import { createStore } from '~/ide/stores';
import Bar from '~/ide/components/file_templates/bar.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore, file } from '../../helpers';
describe('IDE file templates bar component', () => {
let Component;
let vm;
beforeAll(() => {
Component = Vue.extend(Bar);
});
beforeEach(() => {
const store = createStore();
store.state.openFiles.push({
...file('file'),
opened: true,
active: true,
});
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('template type dropdown', () => {
it('renders dropdown component', () => {
expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
});
it('calls setSelectedTemplateType when clicking item', () => {
spyOn(vm, 'setSelectedTemplateType').and.stub();
vm.$el.querySelector('.dropdown-content button').click();
expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
name: '.gitlab-ci.yml',
key: 'gitlab_ci_ymls',
});
});
});
describe('template dropdown', () => {
beforeEach(done => {
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
},
];
vm.$store.state.fileTemplates.selectedTemplateType = {
name: '.gitlab-ci.yml',
key: 'gitlab_ci_ymls',
};
vm.$nextTick(done);
});
it('renders dropdown component', () => {
expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
});
it('calls fetchTemplate on click', () => {
spyOn(vm, 'fetchTemplate').and.stub();
vm.$el
.querySelectorAll('.dropdown-content')[1]
.querySelector('button')
.click();
expect(vm.fetchTemplate).toHaveBeenCalledWith({
name: 'test',
});
});
});
it('shows undo button if updateSuccess is true', done => {
vm.$store.state.fileTemplates.updateSuccess = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
done();
});
});
it('calls undoFileTemplate when clicking undo button', () => {
spyOn(vm, 'undoFileTemplate').and.stub();
vm.$el.querySelector('.btn-default').click();
expect(vm.undoFileTemplate).toHaveBeenCalled();
});
it('calls setSelectedTemplateType if activeFile name matches a template', done => {
const fileName = '.gitlab-ci.yml';
spyOn(vm, 'setSelectedTemplateType');
vm.$store.state.openFiles[0].name = fileName;
vm.setInitialType();
vm.$nextTick(() => {
expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
name: fileName,
key: 'gitlab_ci_ymls',
});
done();
});
});
});
import $ from 'jquery';
import Vue from 'vue';
import { createStore } from '~/ide/stores';
import Dropdown from '~/ide/components/file_templates/dropdown.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('IDE file templates dropdown component', () => {
let Component;
let vm;
beforeAll(() => {
Component = Vue.extend(Dropdown);
});
beforeEach(() => {
const store = createStore();
vm = createComponentWithStore(Component, store, {
label: 'Test',
}).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
describe('async', () => {
beforeEach(() => {
vm.isAsyncData = true;
});
it('calls async store method on Bootstrap dropdown event', () => {
spyOn(vm, 'fetchTemplateTypes').and.stub();
$(vm.$el).trigger('show.bs.dropdown');
expect(vm.fetchTemplateTypes).toHaveBeenCalled();
});
it('renders templates when async', done => {
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
},
];
vm.$nextTick(() => {
expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test');
done();
});
});
it('renders loading icon when isLoading is true', done => {
vm.$store.state.fileTemplates.isLoading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
done();
});
});
it('searches template data', () => {
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
},
];
vm.searchable = true;
vm.search = 'hello';
expect(vm.outputData).toEqual([]);
});
it('does not filter data is searchable is false', () => {
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
},
];
vm.search = 'hello';
expect(vm.outputData).toEqual([
{
name: 'test',
},
]);
});
it('calls clickItem on click', done => {
spyOn(vm, 'clickItem').and.stub();
vm.$store.state.fileTemplates.templates = [
{
name: 'test',
},
];
vm.$nextTick(() => {
vm.$el.querySelector('.dropdown-content button').click();
expect(vm.clickItem).toHaveBeenCalledWith({
name: 'test',
});
done();
});
});
it('renders input when searchable is true', done => {
vm.searchable = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
done();
});
});
it('does not render input when searchable is true & showLoading is true', done => {
vm.searchable = true;
vm.$store.state.fileTemplates.isLoading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.dropdown-input')).toBe(null);
done();
});
});
});
describe('sync', () => {
beforeEach(done => {
vm.data = [
{
name: 'test sync',
},
];
vm.$nextTick(done);
});
it('renders props data', () => {
expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync');
});
it('renders input when searchable is true', done => {
vm.searchable = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
done();
});
});
it('calls clickItem on click', done => {
spyOn(vm, 'clickItem').and.stub();
vm.$nextTick(() => {
vm.$el.querySelector('.dropdown-content button').click();
expect(vm.clickItem).toHaveBeenCalledWith({
name: 'test sync',
});
done();
});
});
it('searches template data', () => {
vm.searchable = true;
vm.search = 'hello';
expect(vm.outputData).toEqual([]);
});
it('does not filter data is searchable is false', () => {
vm.search = 'hello';
expect(vm.outputData).toEqual([
{
name: 'test sync',
},
]);
});
it('renders dropdown title', done => {
vm.title = 'Test title';
vm.$nextTick(() => {
expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title');
done();
});
});
});
});
...@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => { ...@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => {
.then(vm.$nextTick) .then(vm.$nextTick)
.then(() => { .then(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
'No changes', 'There are no unstaged changes',
); );
}) })
.then(done) .then(done)
...@@ -133,7 +133,7 @@ describe('RepoCommitSection', () => { ...@@ -133,7 +133,7 @@ describe('RepoCommitSection', () => {
}); });
it('discards a single file', done => { it('discards a single file', done => {
vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click(); vm.$el.querySelector('.multi-file-commit-list li:first-child .js-modal-primary-action').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
......
...@@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state'; ...@@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state';
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state';
import branchesState from '~/ide/stores/modules/branches/state'; import branchesState from '~/ide/stores/modules/branches/state';
import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
export const resetStore = store => { export const resetStore = store => {
const newState = { const newState = {
...@@ -13,6 +14,7 @@ export const resetStore = store => { ...@@ -13,6 +14,7 @@ export const resetStore = store => {
mergeRequests: mergeRequestsState(), mergeRequests: mergeRequestsState(),
pipelines: pipelinesState(), pipelines: pipelinesState(),
branches: branchesState(), branches: branchesState(),
fileTemplates: fileTemplatesState(),
}; };
store.replaceState(newState); store.replaceState(newState);
}; };
......
...@@ -692,21 +692,6 @@ describe('IDE store file actions', () => { ...@@ -692,21 +692,6 @@ describe('IDE store file actions', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('calls scrollToTab', done => {
const scrollToTabSpy = jasmine.createSpy('scrollToTab');
const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
store
.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
})
.then(done)
.catch(done.fail);
});
}); });
describe('removePendingTab', () => { describe('removePendingTab', () => {
......
...@@ -305,7 +305,11 @@ describe('Multi-file store actions', () => { ...@@ -305,7 +305,11 @@ describe('Multi-file store actions', () => {
describe('stageAllChanges', () => { describe('stageAllChanges', () => {
it('adds all files from changedFiles to stagedFiles', done => { it('adds all files from changedFiles to stagedFiles', done => {
store.state.changedFiles.push(file(), file('new')); const openFile = { ...file(), path: 'test' };
store.state.openFiles.push(openFile);
store.state.stagedFiles.push(openFile);
store.state.changedFiles.push(openFile, file('new'));
testAction( testAction(
stageAllChanges, stageAllChanges,
...@@ -316,7 +320,12 @@ describe('Multi-file store actions', () => { ...@@ -316,7 +320,12 @@ describe('Multi-file store actions', () => {
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
], ],
[], [
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'staged' },
},
],
done, done,
); );
}); });
...@@ -324,7 +333,11 @@ describe('Multi-file store actions', () => { ...@@ -324,7 +333,11 @@ describe('Multi-file store actions', () => {
describe('unstageAllChanges', () => { describe('unstageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', done => { it('removes all files from stagedFiles after unstaging', done => {
store.state.stagedFiles.push(file(), file('new')); const openFile = { ...file(), path: 'test' };
store.state.openFiles.push(openFile);
store.state.changedFiles.push(openFile);
store.state.stagedFiles.push(openFile, file('new'));
testAction( testAction(
unstageAllChanges, unstageAllChanges,
...@@ -334,7 +347,12 @@ describe('Multi-file store actions', () => { ...@@ -334,7 +347,12 @@ describe('Multi-file store actions', () => {
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
], ],
[], [
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'unstaged' },
},
],
done, done,
); );
}); });
......
...@@ -148,14 +148,66 @@ describe('IDE file templates actions', () => { ...@@ -148,14 +148,66 @@ describe('IDE file templates actions', () => {
}); });
describe('setSelectedTemplateType', () => { describe('setSelectedTemplateType', () => {
it('commits SET_SELECTED_TEMPLATE_TYPE', done => { it('commits SET_SELECTED_TEMPLATE_TYPE', () => {
testAction( const commit = jasmine.createSpy('commit');
actions.setSelectedTemplateType, const options = {
'test', commit,
state, dispatch() {},
[{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }], rootGetters: {
[], activeFile: {
done, name: 'test',
prevPath: '',
},
},
};
actions.setSelectedTemplateType(options, { name: 'test' });
expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' });
});
it('dispatches discardFileChanges if prevPath matches templates name', () => {
const dispatch = jasmine.createSpy('dispatch');
const options = {
commit() {},
dispatch,
rootGetters: {
activeFile: {
name: 'test',
path: 'test',
prevPath: 'test',
},
},
};
actions.setSelectedTemplateType(options, { name: 'test' });
expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true });
});
it('dispatches renameEntry if file name doesnt match', () => {
const dispatch = jasmine.createSpy('dispatch');
const options = {
commit() {},
dispatch,
rootGetters: {
activeFile: {
name: 'oldtest',
path: 'oldtest',
prevPath: '',
},
},
};
actions.setSelectedTemplateType(options, { name: 'test' });
expect(dispatch).toHaveBeenCalledWith(
'renameEntry',
{
path: 'oldtest',
name: 'test',
},
{ root: true },
); );
}); });
}); });
...@@ -332,5 +384,20 @@ describe('IDE file templates actions', () => { ...@@ -332,5 +384,20 @@ describe('IDE file templates actions', () => {
expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false);
}); });
it('dispatches discardFileChanges if file has prevPath', () => {
const dispatch = jasmine.createSpy('dispatch');
const rootGetters = {
activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' },
};
actions.undoFileTemplate({ dispatch, commit() {}, rootGetters });
expect(dispatch.calls.mostRecent().args).toEqual([
'discardFileChanges',
'test',
{ root: true },
]);
});
}); });
}); });
import createState from '~/ide/stores/state';
import { activityBarViews } from '~/ide/constants';
import * as getters from '~/ide/stores/modules/file_templates/getters'; import * as getters from '~/ide/stores/modules/file_templates/getters';
describe('IDE file templates getters', () => { describe('IDE file templates getters', () => {
...@@ -8,22 +10,49 @@ describe('IDE file templates getters', () => { ...@@ -8,22 +10,49 @@ describe('IDE file templates getters', () => {
}); });
describe('showFileTemplatesBar', () => { describe('showFileTemplatesBar', () => {
it('finds template type by name', () => { let rootState;
beforeEach(() => {
rootState = createState();
});
it('returns true if template is found and currentActivityView is edit', () => {
rootState.currentActivityView = activityBarViews.edit;
expect( expect(
getters.showFileTemplatesBar(null, { getters.showFileTemplatesBar(
null,
{
templateTypes: getters.templateTypes(), templateTypes: getters.templateTypes(),
})('LICENSE'), },
).toEqual({ rootState,
name: 'LICENSE', )('LICENSE'),
key: 'licenses', ).toBe(true);
}); });
it('returns false if template is found and currentActivityView is not edit', () => {
rootState.currentActivityView = activityBarViews.commit;
expect(
getters.showFileTemplatesBar(
null,
{
templateTypes: getters.templateTypes(),
},
rootState,
)('LICENSE'),
).toBe(false);
}); });
it('returns undefined if not found', () => { it('returns undefined if not found', () => {
expect( expect(
getters.showFileTemplatesBar(null, { getters.showFileTemplatesBar(
null,
{
templateTypes: getters.templateTypes(), templateTypes: getters.templateTypes(),
})('test'), },
rootState,
)('test'),
).toBe(undefined); ).toBe(undefined);
}); });
}); });
......
...@@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => { ...@@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => {
expect(localState.entries.parentPath.tree.length).toBe(1); expect(localState.entries.parentPath.tree.length).toBe(1);
}); });
it('adds to openFiles if previously opened', () => {
localState.entries.oldPath.opened = true;
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
expect(localState.openFiles).toEqual([localState.entries.newPath]);
});
}); });
}); });
...@@ -35,7 +35,7 @@ const defaultValuesComponent = { ...@@ -35,7 +35,7 @@ const defaultValuesComponent = {
unitOfDisplay: 'ms', unitOfDisplay: 'ms',
currentDataIndex: 0, currentDataIndex: 0,
legendTitle: 'Average', legendTitle: 'Average',
currentCoordinates: [], currentCoordinates: {},
}; };
const deploymentFlagData = { const deploymentFlagData = {
......
...@@ -113,6 +113,9 @@ describe('Graph', () => { ...@@ -113,6 +113,9 @@ describe('Graph', () => {
projectPath, projectPath,
}); });
// simulate moving mouse over data series
component.seriesUnderMouse = component.timeSeries;
component.positionFlag(); component.positionFlag();
expect(component.currentData).toBe(component.timeSeries[0].values[10]); expect(component.currentData).toBe(component.timeSeries[0].values[10]);
}); });
......
...@@ -189,9 +189,9 @@ describe API::Helpers::Pagination do ...@@ -189,9 +189,9 @@ describe API::Helpers::Pagination do
it 'it returns the right link to the next page' do it 'it returns the right link to the next page' do
allow(subject).to receive(:params) allow(subject).to receive(:params)
.and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
expect_header('X-Per-Page', '2') expect_header('X-Per-Page', '2')
expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2") expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2")
expect_header('Link', anything) do |_key, val| expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"') expect(val).to include('rel="next"')
end end
......
...@@ -4,13 +4,16 @@ describe Banzai::Filter::SpacedLinkFilter do ...@@ -4,13 +4,16 @@ describe Banzai::Filter::SpacedLinkFilter do
include FilterSpecHelper include FilterSpecHelper
let(:link) { '[example](page slug)' } let(:link) { '[example](page slug)' }
let(:image) { '![example](img test.jpg)' }
context 'when a link is detected' do
it 'converts slug with spaces to a link' do it 'converts slug with spaces to a link' do
doc = filter("See #{link}") doc = filter("See #{link}")
expect(doc.at_css('a').text).to eq 'example' expect(doc.at_css('a').text).to eq 'example'
expect(doc.at_css('a')['href']).to eq 'page%20slug' expect(doc.at_css('a')['href']).to eq 'page%20slug'
expect(doc.at_css('p')).to eq nil expect(doc.at_css('a')['title']).to be_nil
expect(doc.at_css('p')).to be_nil
end end
it 'converts slug with spaces and a title to a link' do it 'converts slug with spaces and a title to a link' do
...@@ -20,7 +23,7 @@ describe Banzai::Filter::SpacedLinkFilter do ...@@ -20,7 +23,7 @@ describe Banzai::Filter::SpacedLinkFilter do
expect(doc.at_css('a').text).to eq 'example' expect(doc.at_css('a').text).to eq 'example'
expect(doc.at_css('a')['href']).to eq 'page%20slug' expect(doc.at_css('a')['href']).to eq 'page%20slug'
expect(doc.at_css('a')['title']).to eq 'title' expect(doc.at_css('a')['title']).to eq 'title'
expect(doc.at_css('p')).to eq nil expect(doc.at_css('p')).to be_nil
end end
it 'does nothing when markdown_engine is redcarpet' do it 'does nothing when markdown_engine is redcarpet' do
...@@ -32,20 +35,41 @@ describe Banzai::Filter::SpacedLinkFilter do ...@@ -32,20 +35,41 @@ describe Banzai::Filter::SpacedLinkFilter do
link = '[](page slug)' link = '[](page slug)'
doc = filter("See #{link}") doc = filter("See #{link}")
expect(doc.at_css('a')).to eq nil expect(doc.at_css('a')).to be_nil
end end
it 'does nothing with an empty slug' do it 'does nothing with an empty slug' do
link = '[example]()' link = '[example]()'
doc = filter("See #{link}") doc = filter("See #{link}")
expect(doc.at_css('a')).to eq nil expect(doc.at_css('a')).to be_nil
end
end
context 'when an image is detected' do
it 'converts slug with spaces to an iamge' do
doc = filter("See #{image}")
expect(doc.at_css('img')['src']).to eq 'img%20test.jpg'
expect(doc.at_css('img')['alt']).to eq 'example'
expect(doc.at_css('p')).to be_nil
end
it 'converts slug with spaces and a title to an image' do
image = '![example](img test.jpg "title")'
doc = filter("See #{image}")
expect(doc.at_css('img')['src']).to eq 'img%20test.jpg'
expect(doc.at_css('img')['alt']).to eq 'example'
expect(doc.at_css('img')['title']).to eq 'title'
expect(doc.at_css('p')).to be_nil
end
end end
it 'converts multiple URLs' do it 'converts multiple URLs' do
link1 = '[first](slug one)' link1 = '[first](slug one)'
link2 = '[second](http://example.com/slug two)' link2 = '[second](http://example.com/slug two)'
doc = filter("See #{link1} and #{link2}") doc = filter("See #{link1} and #{image} and #{link2}")
found_links = doc.css('a') found_links = doc.css('a')
...@@ -54,6 +78,12 @@ describe Banzai::Filter::SpacedLinkFilter do ...@@ -54,6 +78,12 @@ describe Banzai::Filter::SpacedLinkFilter do
expect(found_links[0]['href']).to eq 'slug%20one' expect(found_links[0]['href']).to eq 'slug%20one'
expect(found_links[1].text).to eq 'second' expect(found_links[1].text).to eq 'second'
expect(found_links[1]['href']).to eq 'http://example.com/slug%20two' expect(found_links[1]['href']).to eq 'http://example.com/slug%20two'
found_images = doc.css('img')
expect(found_images.size).to eq(1)
expect(found_images[0]['src']).to eq 'img%20test.jpg'
expect(found_images[0]['alt']).to eq 'example'
end end
described_class::IGNORE_PARENTS.each do |elem| described_class::IGNORE_PARENTS.each do |elem|
......
...@@ -178,4 +178,25 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -178,4 +178,25 @@ describe Banzai::Pipeline::WikiPipeline do
end end
end end
end end
describe 'videos' do
let(:namespace) { create(:namespace, name: "wiki_link_ns") }
let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
it 'generates video html structure' do
markdown = "![video_file](video_file_name.mp4)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"')
end
it 'rewrites and replaces video links names with white spaces to %20' do
markdown = "![video file](video file name.mp4)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
end
end
end end
...@@ -7,6 +7,7 @@ describe Forever do ...@@ -7,6 +7,7 @@ describe Forever do
context 'when using PostgreSQL' do context 'when using PostgreSQL' do
it 'should return Postgresql future date' do it 'should return Postgresql future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true) allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(subject).to eq(described_class::POSTGRESQL_DATE) expect(subject).to eq(described_class::POSTGRESQL_DATE)
end end
end end
...@@ -14,6 +15,7 @@ describe Forever do ...@@ -14,6 +15,7 @@ describe Forever do
context 'when using MySQL' do context 'when using MySQL' do
it 'should return MySQL future date' do it 'should return MySQL future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false) allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(subject).to eq(described_class::MYSQL_DATE) expect(subject).to eq(described_class::MYSQL_DATE)
end end
end end
......
...@@ -62,14 +62,17 @@ describe Gitlab::ContributionsCalendar do ...@@ -62,14 +62,17 @@ describe Gitlab::ContributionsCalendar do
expect(calendar.activity_dates).to eq(last_week => 2, today => 1) expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
end end
it "only shows private events to authorized users" do context "when the user has opted-in for private contributions" do
it "shows private and public events to all users" do
user.update_column(:include_private_contributions, true)
create_event(private_project, today) create_event(private_project, today)
create_event(feature_project, today) create_event(public_project, today)
expect(calendar.activity_dates[today]).to eq(0) expect(calendar.activity_dates[today]).to eq(1)
expect(calendar(user).activity_dates[today]).to eq(0) expect(calendar(user).activity_dates[today]).to eq(1)
expect(calendar(contributor).activity_dates[today]).to eq(2) expect(calendar(contributor).activity_dates[today]).to eq(2)
end end
end
it "counts the diff notes on merge request" do it "counts the diff notes on merge request" do
create_event(public_project, today, 0, Event::COMMENTED, :diff_note_on_merge_request) create_event(public_project, today, 0, Event::COMMENTED, :diff_note_on_merge_request)
...@@ -128,7 +131,7 @@ describe Gitlab::ContributionsCalendar do ...@@ -128,7 +131,7 @@ describe Gitlab::ContributionsCalendar do
e3 = create_event(feature_project, today) e3 = create_event(feature_project, today)
create_event(public_project, last_week) create_event(public_project, last_week)
expect(calendar.events_by_date(today)).to contain_exactly(e1) expect(calendar.events_by_date(today)).to contain_exactly(e1, e3)
expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Diff::HighlightCache do
let(:merge_request) { create(:merge_request_with_diffs) }
subject(:cache) { described_class.new(merge_request.diffs, backend: backend) }
describe '#decorate' do
let(:backend) { double('backend').as_null_object }
# Manually creates a Diff::File object to avoid triggering the cache on
# the FileCollection::MergeRequestDiff
let(:diff_file) do
diffs = merge_request.diffs
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
Gitlab::Diff::File.new(raw_diff,
repository: diffs.project.repository,
diff_refs: diffs.diff_refs,
fallback_diff_refs: diffs.fallback_diff_refs)
end
it 'does not calculate highlighting when reading from cache' do
cache.write_if_empty
cache.decorate(diff_file)
expect_any_instance_of(Gitlab::Diff::Highlight).not_to receive(:highlight)
diff_file.highlighted_diff_lines
end
it 'assigns highlighted diff lines to the DiffFile' do
cache.write_if_empty
cache.decorate(diff_file)
expect(diff_file.highlighted_diff_lines.size).to be > 5
end
it 'submits a single reading from the cache' do
cache.decorate(diff_file)
cache.decorate(diff_file)
expect(backend).to have_received(:read).with(cache.key).once
end
end
describe '#write_if_empty' do
let(:backend) { double('backend', read: {}).as_null_object }
it 'submits a single writing to the cache' do
cache.write_if_empty
cache.write_if_empty
expect(backend).to have_received(:write).with(cache.key,
hash_including('CHANGELOG-false-false-false'),
expires_in: 1.week).once
end
end
describe '#clear' do
let(:backend) { double('backend').as_null_object }
it 'clears cache' do
cache.clear
expect(backend).to have_received(:delete).with(cache.key)
end
end
end
...@@ -591,6 +591,10 @@ describe Gitlab::Git::Repository, :seed_helper do ...@@ -591,6 +591,10 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(repository.find_remote_root_ref('origin')).to eq 'master' expect(repository.find_remote_root_ref('origin')).to eq 'master'
end end
it 'returns UTF-8' do
expect(repository.find_remote_root_ref('origin')).to be_utf8
end
it 'returns nil when remote name is nil' do it 'returns nil when remote name is nil' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService) expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.not_to receive(:find_remote_root_ref) .not_to receive(:find_remote_root_ref)
......
...@@ -19,7 +19,14 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -19,7 +19,14 @@ describe Gitlab::GitalyClient::CommitService do
right_commit_id: commit.id, right_commit_id: commit.id,
collapse_diffs: false, collapse_diffs: false,
enforce_limits: true, enforce_limits: true,
**Gitlab::Git::DiffCollection.collection_limits.to_h # Tests limitation parameters explicitly
max_files: 100,
max_lines: 5000,
max_bytes: 512000,
safe_max_files: 100,
safe_max_lines: 5000,
safe_max_bytes: 512000,
max_patch_bytes: 102400
) )
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
...@@ -37,7 +44,14 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -37,7 +44,14 @@ describe Gitlab::GitalyClient::CommitService do
right_commit_id: initial_commit.id, right_commit_id: initial_commit.id,
collapse_diffs: false, collapse_diffs: false,
enforce_limits: true, enforce_limits: true,
**Gitlab::Git::DiffCollection.collection_limits.to_h # Tests limitation parameters explicitly
max_files: 100,
max_lines: 5000,
max_bytes: 512000,
safe_max_files: 100,
safe_max_lines: 5000,
safe_max_bytes: 512000,
max_patch_bytes: 102400
) )
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
......
...@@ -54,6 +54,15 @@ describe Gitlab::GitalyClient::RemoteService do ...@@ -54,6 +54,15 @@ describe Gitlab::GitalyClient::RemoteService do
expect(client.find_remote_root_ref('origin')).to eq 'master' expect(client.find_remote_root_ref('origin')).to eq 'master'
end end
it 'ensure ref is a valid UTF-8 string' do
expect_any_instance_of(Gitaly::RemoteService::Stub)
.to receive(:find_remote_root_ref)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return(double(ref: "an_invalid_ref_\xE5"))
expect(client.find_remote_root_ref('origin')).to eq "an_invalid_ref_å"
end
end end
describe '#update_remote_mirror' do describe '#update_remote_mirror' do
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180906051323_remove_orphaned_label_links.rb')
describe RemoveOrphanedLabelLinks, :migration do
let(:label_links) { table(:label_links) }
let(:labels) { table(:labels) }
let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
let(:label) { create_label }
context 'add foreign key on label_id' do
let!(:label_link_with_label) { create_label_link(label_id: label.id) }
let!(:label_link_without_label) { create_label_link(label_id: nil) }
it 'removes orphaned labels without corresponding label' do
expect { migrate! }.to change { LabelLink.count }.from(2).to(1)
end
it 'does not remove entries with valid label_id' do
expect { migrate! }.not_to change { label_link_with_label.reload }
end
end
def create_label(**opts)
labels.create!(
project_id: project.id,
**opts
)
end
def create_label_link(**opts)
label_links.create!(
target_id: 1,
target_type: 'Issue',
**opts
)
end
end
...@@ -231,12 +231,12 @@ describe JiraService do ...@@ -231,12 +231,12 @@ describe JiraService do
end end
it 'logs exception when transition id is not valid' do it 'logs exception when transition id is not valid' do
allow(Rails.logger).to receive(:info) allow(@jira_service).to receive(:log_error)
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request') WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request")
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request') expect(@jira_service).to have_received(:log_error).with("Issue transition failed", error: "Bad Request", client_url: "http://jira.example.com")
end end
it 'calls the api with jira_issue_transition_id' do it 'calls the api with jira_issue_transition_id' do
......
...@@ -345,4 +345,31 @@ describe Service do ...@@ -345,4 +345,31 @@ describe Service do
expect(service.api_field_names).to eq(['safe_field']) expect(service.api_field_names).to eq(['safe_field'])
end end
end end
context 'logging' do
let(:project) { create(:project) }
let(:service) { create(:service, project: project) }
let(:test_message) { "test message" }
let(:arguments) do
{
service_class: service.class.name,
project_path: project.full_path,
project_id: project.id,
message: test_message,
additional_argument: 'some argument'
}
end
it 'logs info messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
service.log_info(test_message, additional_argument: 'some argument')
end
it 'logs error messages using json logger' do
expect(Gitlab::JsonLogger).to receive(:error).with(arguments)
service.log_error(test_message, additional_argument: 'some argument')
end
end
end end
...@@ -57,6 +57,7 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin ...@@ -57,6 +57,7 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin
expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
subject.execute subject.execute
end end
end end
......
...@@ -688,7 +688,7 @@ describe SystemNoteService do ...@@ -688,7 +688,7 @@ describe SystemNoteService do
let(:jira_tracker) { project.jira_service } let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit } let(:commit) { project.commit }
let(:comment_url) { jira_api_comment_url(jira_issue.id) } let(:comment_url) { jira_api_comment_url(jira_issue.id) }
let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." } let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." }
before do before do
stub_jira_urls(jira_issue.id) stub_jira_urls(jira_issue.id)
......
...@@ -88,8 +88,30 @@ describe Wikis::CreateAttachmentService do ...@@ -88,8 +88,30 @@ describe Wikis::CreateAttachmentService do
end end
end end
describe 'validations' do describe '#parse_file_name' do
context 'when file_name' do context 'when file_name' do
context 'has white spaces' do
let(:file_name) { 'file with spaces' }
it "replaces all of them with '_'" do
result = service.execute
expect(result[:status]).to eq :success
expect(result[:result][:file_name]).to eq 'file_with_spaces'
end
end
context 'has other invalid characters' do
let(:file_name) { "file\twith\tinvalid chars" }
it "replaces all of them with '_'" do
result = service.execute
expect(result[:status]).to eq :success
expect(result[:result][:file_name]).to eq 'file_with_invalid_chars'
end
end
context 'is not present' do context 'is not present' do
let(:file_name) { nil } let(:file_name) { nil }
......
...@@ -6,6 +6,8 @@ describe 'gitlab:cleanup rake tasks' do ...@@ -6,6 +6,8 @@ describe 'gitlab:cleanup rake tasks' do
end end
describe 'cleanup namespaces and repos' do describe 'cleanup namespaces and repos' do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:storage) { storages.keys.first }
let(:storages) do let(:storages) do
{ {
'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
...@@ -17,53 +19,56 @@ describe 'gitlab:cleanup rake tasks' do ...@@ -17,53 +19,56 @@ describe 'gitlab:cleanup rake tasks' do
end end
before do before do
FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end end
after do after do
FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) Gitlab::GitalyClient::StorageService.new(storage).delete_all_repositories
end end
describe 'cleanup:repos' do describe 'cleanup:repos' do
before do before do
FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git')) gitlab_shell.add_namespace(storage, 'broken/project.git')
FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
end end
it 'moves it to an orphaned path' do it 'moves it to an orphaned path' do
now = Time.now
Timecop.freeze(now) do
run_rake_task('gitlab:cleanup:repos') run_rake_task('gitlab:cleanup:repos')
repo_list = Dir['tmp/tests/default_storage/broken/*'] repo_list = Gitlab::GitalyClient::StorageService.new(storage).list_directories(depth: 0)
expect(repo_list.first).to include('+orphaned+') expect(repo_list.last).to include("broken+orphaned+#{now.to_i}")
end
end end
it 'ignores @hashed repos' do it 'ignores @hashed repos' do
run_rake_task('gitlab:cleanup:repos') run_rake_task('gitlab:cleanup:repos')
expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
end end
end end
describe 'cleanup:dirs' do describe 'cleanup:dirs' do
it 'removes missing namespaces' do it 'removes missing namespaces' do
FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_1/project.git")) gitlab_shell.add_namespace(storage, "namespace_1/project.git")
FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_2/project.git")) gitlab_shell.add_namespace(storage, "namespace_2/project.git")
allow(Namespace).to receive(:pluck).and_return('namespace_1') allow(Namespace).to receive(:pluck).and_return(['namespace_1'])
stub_env('REMOVE', 'true') stub_env('REMOVE', 'true')
run_rake_task('gitlab:cleanup:dirs') run_rake_task('gitlab:cleanup:dirs')
expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_1'))).to be_truthy expect(gitlab_shell.exists?(storage, 'namespace_1')).to be(true)
expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_2'))).to be_falsey expect(gitlab_shell.exists?(storage, 'namespace_2')).to be(false)
end end
it 'ignores @hashed directory' do it 'ignores @hashed directory' do
FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
run_rake_task('gitlab:cleanup:dirs') run_rake_task('gitlab:cleanup:dirs')
expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
end end
end end
end end
......
...@@ -26,6 +26,20 @@ describe NamespaceFileUploader do ...@@ -26,6 +26,20 @@ describe NamespaceFileUploader do
upload_path: IDENTIFIER upload_path: IDENTIFIER
end end
context '.base_dir' do
it 'returns local storage base_dir without store param' do
expect(described_class.base_dir(group)).to eq("uploads/-/system/namespace/#{group.id}")
end
it 'returns local storage base_dir when store param is Store::LOCAL' do
expect(described_class.base_dir(group, ObjectStorage::Store::LOCAL)).to eq("uploads/-/system/namespace/#{group.id}")
end
it 'returns remote base_dir when store param is Store::REMOTE' do
expect(described_class.base_dir(group, ObjectStorage::Store::REMOTE)).to eq("namespace/#{group.id}")
end
end
describe "#migrate!" do describe "#migrate!" do
before do before do
uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt'))) uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt')))
......
...@@ -78,11 +78,7 @@ ...@@ -78,11 +78,7 @@
lodash "^4.2.0" lodash "^4.2.0"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@gitlab-org/gitlab-svgs@^1.23.0": "@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0":
version "1.27.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.27.0.tgz#638e70399ebd59e503732177316bb9a18bf7a13f"
"@gitlab-org/gitlab-svgs@^1.29.0":
version "1.29.0" version "1.29.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70"
...@@ -307,11 +303,7 @@ acorn@^3.0.4: ...@@ -307,11 +303,7 @@ acorn@^3.0.4:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7"
acorn@^5.6.2:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
...@@ -553,13 +545,7 @@ async@1.x, async@^1.4.0, async@^1.5.2: ...@@ -553,13 +545,7 @@ async@1.x, async@^1.4.0, async@^1.5.2:
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.0.0, async@^2.1.4: async@^2.0.0, async@^2.1.4, async@~2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
async@~2.6.0:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
dependencies: dependencies:
...@@ -1380,13 +1366,9 @@ bootstrap-vue@^2.0.0-rc.11: ...@@ -1380,13 +1366,9 @@ bootstrap-vue@^2.0.0-rc.11:
popper.js "^1.12.9" popper.js "^1.12.9"
vue-functional-data-merge "^2.0.5" vue-functional-data-merge "^2.0.5"
bootstrap@^4.1.1: bootstrap@^4.1.1, bootstrap@~4.1.1:
version "4.1.3" version "4.1.2"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.2.tgz#aee2a93472e61c471fc79fb475531dcbc87de326"
bootstrap@~4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb"
boxen@^1.2.1: boxen@^1.2.1:
version "1.3.0" version "1.3.0"
...@@ -1681,25 +1663,7 @@ check-types@^7.3.0: ...@@ -1681,25 +1663,7 @@ check-types@^7.3.0:
version "7.3.0" version "7.3.0"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
chokidar@^2.0.0, chokidar@^2.0.2: chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3:
version "2.0.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7"
dependencies:
anymatch "^2.0.0"
async-each "^1.0.0"
braces "^2.3.0"
glob-parent "^3.1.0"
inherits "^2.0.1"
is-binary-path "^1.0.0"
is-glob "^4.0.0"
normalize-path "^2.1.1"
path-is-absolute "^1.0.0"
readdirp "^2.0.0"
upath "^1.0.0"
optionalDependencies:
fsevents "^1.0.0"
chokidar@^2.0.3:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
dependencies: dependencies:
...@@ -2784,15 +2748,7 @@ engine.io@~3.1.0: ...@@ -2784,15 +2748,7 @@ engine.io@~3.1.0:
optionalDependencies: optionalDependencies:
uws "~9.14.0" uws "~9.14.0"
enhanced-resolve@^4.0.0: enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
dependencies:
graceful-fs "^4.1.2"
memory-fs "^0.4.0"
tapable "^1.0.0"
enhanced-resolve@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
dependencies: dependencies:
...@@ -2816,13 +2772,7 @@ entities@^1.1.1, entities@~1.1.1: ...@@ -2816,13 +2772,7 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
errno@^0.1.3: errno@^0.1.3, errno@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
dependencies:
prr "~0.0.0"
errno@^0.1.4:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
dependencies: dependencies:
...@@ -3470,7 +3420,7 @@ fs.realpath@^1.0.0: ...@@ -3470,7 +3420,7 @@ fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0, fsevents@^1.2.2: fsevents@^1.2.2:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
dependencies: dependencies:
...@@ -4490,11 +4440,7 @@ istanbul-api@^1.1.14: ...@@ -4490,11 +4440,7 @@ istanbul-api@^1.1.14:
mkdirp "^0.5.1" mkdirp "^0.5.1"
once "^1.4.0" once "^1.4.0"
istanbul-lib-coverage@^1.1.1: istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.2.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
istanbul-lib-coverage@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
...@@ -4945,7 +4891,7 @@ lodash@4.17.4: ...@@ -4945,7 +4891,7 @@ lodash@4.17.4:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
...@@ -6003,15 +5949,7 @@ postcss-value-parser@^3.3.0: ...@@ -6003,15 +5949,7 @@ postcss-value-parser@^3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20: postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20, postcss@^6.0.23:
version "6.0.22"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^6.0.23:
version "6.0.23" version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
dependencies: dependencies:
...@@ -6095,10 +6033,6 @@ proxy-from-env@^1.0.0: ...@@ -6095,10 +6033,6 @@ proxy-from-env@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
prr@~1.0.1: prr@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
...@@ -6276,16 +6210,16 @@ read-pkg@^2.0.0: ...@@ -6276,16 +6210,16 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2" normalize-package-data "^2.3.2"
path-type "^2.0.0" path-type "^2.0.0"
"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: "readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6:
version "2.3.4" version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies: dependencies:
core-util-is "~1.0.0" core-util-is "~1.0.0"
inherits "~2.0.3" inherits "~2.0.3"
isarray "~1.0.0" isarray "~1.0.0"
process-nextick-args "~2.0.0" process-nextick-args "~2.0.0"
safe-buffer "~5.1.1" safe-buffer "~5.1.1"
string_decoder "~1.0.3" string_decoder "~1.1.1"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
...@@ -6297,18 +6231,6 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": ...@@ -6297,18 +6231,6 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
isarray "0.0.1" isarray "0.0.1"
string_decoder "~0.10.x" string_decoder "~0.10.x"
readable-stream@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@~2.0.5, readable-stream@~2.0.6: readable-stream@~2.0.5, readable-stream@~2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
...@@ -7021,14 +6943,10 @@ source-map@^0.4.4: ...@@ -7021,14 +6943,10 @@ source-map@^0.4.4:
dependencies: dependencies:
amdefine ">=0.0.4" amdefine ">=0.0.4"
source-map@^0.5.0, source-map@^0.5.7, source-map@~0.5.6: source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
source-map@^0.6.1, source-map@~0.6.1: source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
...@@ -7130,11 +7048,7 @@ static-extend@^0.1.1: ...@@ -7130,11 +7048,7 @@ static-extend@^0.1.1:
define-property "^0.2.5" define-property "^0.2.5"
object-copy "^0.1.0" object-copy "^0.1.0"
"statuses@>= 1.3.1 < 2": "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
"statuses@>= 1.4.0 < 2":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
...@@ -7218,12 +7132,6 @@ string_decoder@~0.10.x: ...@@ -7218,12 +7132,6 @@ string_decoder@~0.10.x:
version "0.10.31" version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
safe-buffer "~5.1.0"
stringstream@~0.0.4, stringstream@~0.0.5: stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
...@@ -7621,10 +7529,6 @@ unzip-response@^2.0.1: ...@@ -7621,10 +7529,6 @@ unzip-response@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
upath@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73"
upath@^1.0.5: upath@^1.0.5:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
...@@ -7744,11 +7648,7 @@ validate-npm-package-license@^3.0.1: ...@@ -7744,11 +7648,7 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0" spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0" spdx-expression-parse "~1.0.0"
vary@~1.1.1: vary@~1.1.1, vary@~1.1.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
......
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