Commit 0f541b2d authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-09-07

# Conflicts:
#	app/assets/javascripts/boards/models/list.js
#	app/assets/javascripts/main.js
#	app/assets/stylesheets/pages/groups.scss
#	app/models/concerns/issuable.rb
#	app/models/prometheus_metric.rb
#	app/models/resource_label_event.rb
#	app/presenters/commit_status_presenter.rb
#	app/views/projects/project_members/index.html.haml
#	config/prometheus/common_metrics.yml
#	db/schema.rb
#	doc/api/resource_label_events.md
#	doc/development/prometheus_metrics.md
#	lib/api/entities.rb
#	lib/api/resource_label_events.rb
#	lib/gitlab/ci/status/build/failed.rb
#	lib/gitlab/import_export/import_export.yml
#	locale/gitlab.pot
#	spec/features/projects/members/invite_group_and_members_spec.rb
#	spec/features/projects/members/invite_group_spec.rb
#	spec/features/projects/members/share_with_group_spec.rb
#	spec/features/usage_stats_consent_spec.rb
#	spec/lib/gitlab/import_export/all_models.yml
#	spec/lib/gitlab/import_export/safe_model_attributes.yml
#	spec/requests/api/resource_label_events_spec.rb
#	yarn.lock

[ci skip]
parents 0bb43e71 dc658a18
...@@ -5,7 +5,10 @@ import { __ } from '~/locale'; ...@@ -5,7 +5,10 @@ import { __ } from '~/locale';
import ListLabel from '~/vue_shared/models/label'; import ListLabel from '~/vue_shared/models/label';
import ListAssignee from '~/vue_shared/models/assignee'; import ListAssignee from '~/vue_shared/models/assignee';
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
<<<<<<< HEAD
import ListMilestone from './milestone'; import ListMilestone from './milestone';
=======
>>>>>>> upstream/master
const PER_PAGE = 20; const PER_PAGE = 20;
......
<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"
/> />
{{ titleText }} <strong>
{{ 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">
<icon <file-icon
:name="iconName" :file-name="file.name"
:size="16" class="append-right-8"
:css-classes="iconClass"
/>{{ file.name }} />{{ file.name }}
</span> </span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>
</div>
<component
:is="actionComponent"
:path="file.path"
/>
</div>
</div> </div>
<component
:is="actionComponent"
:path="file.path"
class="d-flex position-absolute"
/>
</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,
}); });
......
...@@ -30,9 +30,12 @@ import './frequent_items'; ...@@ -30,9 +30,12 @@ import './frequent_items';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher'; import initDispatcher from './dispatcher';
import initUsagePingConsent from './usage_ping_consent'; import initUsagePingConsent from './usage_ping_consent';
<<<<<<< HEAD
// EE-only scripts // EE-only scripts
import 'ee/main'; // eslint-disable-line import/first import 'ee/main'; // eslint-disable-line import/first
=======
>>>>>>> upstream/master
// expose jQuery as global (TODO: remove these) // expose jQuery as global (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
......
...@@ -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;
}
...@@ -158,6 +158,7 @@ ...@@ -158,6 +158,7 @@
padding: $gl-padding 0; padding: $gl-padding 0;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
<<<<<<< HEAD
.groups-listing { .groups-listing {
.group-list-tree .group-row:first-child { .group-list-tree .group-row:first-child {
...@@ -194,6 +195,12 @@ ...@@ -194,6 +195,12 @@
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
margin-top: 4px; margin-top: 4px;
=======
.groups-listing {
.group-list-tree .group-row:first-child {
border-top: 0;
>>>>>>> upstream/master
} }
} }
......
...@@ -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,13 @@ module Issuable ...@@ -111,6 +111,13 @@ module Issuable
def allows_multiple_assignees? def allows_multiple_assignees?
false false
end end
<<<<<<< HEAD
=======
def has_multiple_assignees?
assignees.count > 1
end
>>>>>>> upstream/master
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
......
# frozen_string_literal: true # frozen_string_literal: true
class PrometheusMetric < ActiveRecord::Base class PrometheusMetric < ActiveRecord::Base
<<<<<<< HEAD
prepend EE::PrometheusMetric prepend EE::PrometheusMetric
=======
>>>>>>> upstream/master
belongs_to :project, validate: true, inverse_of: :prometheus_metrics belongs_to :project, validate: true, inverse_of: :prometheus_metrics
enum group: { enum group: {
......
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
# This model is not used yet, it will be used for: # This model is not used yet, it will be used for:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 # https://gitlab.com/gitlab-org/gitlab-ce/issues/48483
class ResourceLabelEvent < ActiveRecord::Base class ResourceLabelEvent < ActiveRecord::Base
<<<<<<< HEAD
prepend EE::ResourceLabelEvent prepend EE::ResourceLabelEvent
=======
>>>>>>> upstream/master
include Importable include Importable
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include CacheMarkdownField include CacheMarkdownField
......
...@@ -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
......
...@@ -14,8 +14,11 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -14,8 +14,11 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
presents :build presents :build
<<<<<<< HEAD
prepend ::EE::CommitStatusPresenter prepend ::EE::CommitStatusPresenter
=======
>>>>>>> upstream/master
def self.callout_failure_messages def self.callout_failure_messages
CALLOUT_FAILURE_MESSAGES CALLOUT_FAILURE_MESSAGES
end end
......
...@@ -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")
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
.row.prepend-top-default .row.prepend-top-default
.col-lg-12 .col-lg-12
<<<<<<< HEAD
- if project_can_be_shared? - if project_can_be_shared?
%h4 %h4
Project members Project members
...@@ -33,6 +34,35 @@ ...@@ -33,6 +34,35 @@
.invite-member= render 'projects/project_members/new_project_member', tab_title: 'Add member' .invite-member= render 'projects/project_members/new_project_member', tab_title: 'Add 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
Project members
- if can?(current_user, :admin_project_member, @project)
%p
You can invite a new member to
%strong= @project.name
or invite another group.
- else
%p
Members can be added by project
%i Maintainers
or
%i Owners
.light
- if can?(current_user, :admin_project_member, @project)
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
%a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
- if @project.allowed_to_share_with_group?
%li.nav-tab{ role: 'presentation' }
%a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_member', tab_title: 'Invite member'
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_group', tab_title: 'Invite group'
>>>>>>> upstream/master
= render 'shared/members/requests', membership_source: @project, requesters: @requesters = render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix .clearfix
......
%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,25 +8,28 @@ ...@@ -9,25 +8,28 @@
%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.push? - if event.visible_to_user?(current_user)
#{event.action_name} #{event.ref_type} - if event.push?
#{event.action_name} #{event.ref_type}
%strong
- commits_path = project_commits_path(event.project, event.ref_name)
= link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
- else
= event_action_name(event)
%strong
- if event.note?
= link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at
%strong %strong
- commits_path = project_commits_path(event.project, event.ref_name) - if event.project
= link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path = link_to_project(event.project)
- else
= event.project_name
- else - else
= event_action_name(event) made a private contribution
%strong
- if event.note?
= link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at
%strong
- if event.project
= link_to_project event.project
- else
= event.project_name
- 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
...@@ -187,6 +187,19 @@ ...@@ -187,6 +187,19 @@
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 2 weight: 2
queries: queries:
<<<<<<< HEAD:config/prometheus/common_metrics.yml
=======
- id: system_metrics_kubernetes_container_memory_average
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
y_label: "Memory Used per Pod"
required_metrics:
- container_memory_usage_bytes
weight: 2
queries:
>>>>>>> upstream/master:config/prometheus/common_metrics.yml
- id: system_metrics_kubernetes_container_memory_average_canary - id: system_metrics_kubernetes_container_memory_average_canary
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average label: Pod average
...@@ -199,7 +212,11 @@ ...@@ -199,7 +212,11 @@
weight: 1 weight: 1
queries: queries:
- id: system_metrics_kubernetes_container_core_usage - id: system_metrics_kubernetes_container_core_usage
<<<<<<< HEAD:config/prometheus/common_metrics.yml
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
=======
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
>>>>>>> upstream/master:config/prometheus/common_metrics.yml
label: Pod average label: Pod average
unit: "cores" unit: "cores"
- title: "Canary: Core Usage (Pod Average)" - title: "Canary: Core Usage (Pod Average)"
...@@ -213,4 +230,7 @@ ...@@ -213,4 +230,7 @@
label: Pod average label: Pod average
unit: "cores" unit: "cores"
track: canary track: canary
<<<<<<< HEAD:config/prometheus/common_metrics.yml
=======
>>>>>>> upstream/master:config/prometheus/common_metrics.yml
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
...@@ -217,6 +217,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -217,6 +217,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do
t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
t.boolean "user_show_add_ssh_key_message", default: true, null: false t.boolean "user_show_add_ssh_key_message", default: true, null: false
<<<<<<< HEAD
t.integer "custom_project_templates_group_id" t.integer "custom_project_templates_group_id"
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
end end
...@@ -247,6 +248,9 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -247,6 +248,9 @@ ActiveRecord::Schema.define(version: 20180906101639) do
t.integer "user_id", null: false t.integer "user_id", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
=======
t.integer "usage_stats_set_by_user_id"
>>>>>>> upstream/master
end end
add_index "approvers", ["target_id", "target_type"], name: "index_approvers_on_target_id_and_target_type", using: :btree add_index "approvers", ["target_id", "target_type"], name: "index_approvers_on_target_id_and_target_type", using: :btree
...@@ -2262,6 +2266,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -2262,6 +2266,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
<<<<<<< HEAD
create_table "prometheus_alerts", force: :cascade do |t| create_table "prometheus_alerts", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -2276,6 +2281,8 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -2276,6 +2281,8 @@ ActiveRecord::Schema.define(version: 20180906101639) do
add_index "prometheus_alerts", ["project_id", "prometheus_metric_id"], name: "index_prometheus_alerts_on_project_id_and_prometheus_metric_id", unique: true, using: :btree add_index "prometheus_alerts", ["project_id", "prometheus_metric_id"], name: "index_prometheus_alerts_on_project_id_and_prometheus_metric_id", unique: true, using: :btree
add_index "prometheus_alerts", ["prometheus_metric_id"], name: "index_prometheus_alerts_on_prometheus_metric_id", using: :btree add_index "prometheus_alerts", ["prometheus_metric_id"], name: "index_prometheus_alerts_on_prometheus_metric_id", using: :btree
=======
>>>>>>> upstream/master
create_table "prometheus_metrics", force: :cascade do |t| create_table "prometheus_metrics", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
t.string "title", null: false t.string "title", null: false
...@@ -2890,7 +2897,11 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -2890,7 +2897,11 @@ ActiveRecord::Schema.define(version: 20180906101639) do
t.integer "accepted_term_id" t.integer "accepted_term_id"
t.string "feed_token" t.string "feed_token"
t.boolean "private_profile" t.boolean "private_profile"
<<<<<<< HEAD
t.integer "roadmap_layout", limit: 2 t.integer "roadmap_layout", limit: 2
=======
t.boolean "include_private_contributions"
>>>>>>> upstream/master
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
...@@ -2982,11 +2993,15 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -2982,11 +2993,15 @@ ActiveRecord::Schema.define(version: 20180906101639) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
<<<<<<< HEAD
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify
add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade add_foreign_key "approvals", "merge_requests", name: "fk_310d714958", on_delete: :cascade
add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade
=======
add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify
>>>>>>> upstream/master
add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "badges", "projects", on_delete: :cascade add_foreign_key "badges", "projects", on_delete: :cascade
add_foreign_key "board_assignees", "boards", on_delete: :cascade add_foreign_key "board_assignees", "boards", on_delete: :cascade
...@@ -3107,6 +3122,7 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -3107,6 +3122,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
...@@ -3164,11 +3180,15 @@ ActiveRecord::Schema.define(version: 20180906101639) do ...@@ -3164,11 +3180,15 @@ ActiveRecord::Schema.define(version: 20180906101639) do
add_foreign_key "project_mirror_data", "projects", name: "fk_d1aad367d7", on_delete: :cascade add_foreign_key "project_mirror_data", "projects", name: "fk_d1aad367d7", on_delete: :cascade
add_foreign_key "project_repository_states", "projects", on_delete: :cascade add_foreign_key "project_repository_states", "projects", on_delete: :cascade
add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade
<<<<<<< HEAD
add_foreign_key "prometheus_alerts", "environments", on_delete: :cascade add_foreign_key "prometheus_alerts", "environments", on_delete: :cascade
add_foreign_key "prometheus_alerts", "projects", on_delete: :cascade add_foreign_key "prometheus_alerts", "projects", on_delete: :cascade
add_foreign_key "prometheus_alerts", "prometheus_metrics", on_delete: :cascade add_foreign_key "prometheus_alerts", "prometheus_metrics", on_delete: :cascade
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id", name: "fk_98f3d044fe", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id", name: "fk_98f3d044fe", on_delete: :cascade
=======
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
>>>>>>> upstream/master
add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "users" add_foreign_key "protected_branch_merge_access_levels", "users"
add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id", name: "fk_7111b68cdb", on_delete: :cascade add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id", name: "fk_7111b68cdb", 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
......
...@@ -88,6 +88,7 @@ Parameters: ...@@ -88,6 +88,7 @@ Parameters:
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1 curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1
``` ```
<<<<<<< HEAD
## Epics ## Epics
### List group epic label events ### List group epic label events
...@@ -174,6 +175,8 @@ Parameters: ...@@ -174,6 +175,8 @@ Parameters:
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events/107 curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events/107
``` ```
=======
>>>>>>> upstream/master
## Merge requests ## Merge requests
### List project merge request label events ### List project merge request label events
......
...@@ -6,8 +6,12 @@ We strive to support the 2-4 most important metrics for each common system servi ...@@ -6,8 +6,12 @@ We strive to support the 2-4 most important metrics for each common system servi
### Query identifier ### Query identifier
<<<<<<< HEAD
The requirement for adding a new metrics is to make each query to have an unique identifier. The requirement for adding a new metrics is to make each query to have an unique identifier.
Identifier is used to update the metric later when changed. Identifier is used to update the metric later when changed.
=======
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:
>>>>>>> upstream/master
```yaml ```yaml
- group: Response metrics (NGINX Ingress) - group: Response metrics (NGINX Ingress)
...@@ -25,9 +29,16 @@ Identifier is used to update the metric later when changed. ...@@ -25,9 +29,16 @@ 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.
<<<<<<< HEAD
**Note: If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.** **Note: If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.**
**You might want to add additional database migration that makes a decision what to do with removed one.** **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.** **For example: you might be interested in migrating all dependent data to a different metric.**
=======
NOTE: **Note:**
If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.
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.
>>>>>>> upstream/master
```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
......
...@@ -1466,6 +1466,7 @@ module API ...@@ -1466,6 +1466,7 @@ module API
end end
end end
<<<<<<< HEAD
def self.prepend_entity(klass, with: nil) def self.prepend_entity(klass, with: nil)
if with.nil? if with.nil?
raise ArgumentError, 'You need to pass either the :with or :namespace option!' raise ArgumentError, 'You need to pass either the :with or :namespace option!'
...@@ -1487,6 +1488,8 @@ module API ...@@ -1487,6 +1488,8 @@ module API
expose :id, :name, :approval_status expose :id, :name, :approval_status
end end
=======
>>>>>>> upstream/master
class ResourceLabelEvent < Grape::Entity class ResourceLabelEvent < Grape::Entity
expose :id expose :id
expose :user, using: Entities::UserBasic expose :user, using: Entities::UserBasic
......
...@@ -7,7 +7,11 @@ module API ...@@ -7,7 +7,11 @@ module API
before { authenticate! } before { authenticate! }
<<<<<<< HEAD
EVENTABLE_TYPES = [Issue, Epic, MergeRequest].freeze EVENTABLE_TYPES = [Issue, Epic, MergeRequest].freeze
=======
EVENTABLE_TYPES = [Issue, MergeRequest].freeze
>>>>>>> upstream/master
EVENTABLE_TYPES.each do |eventable_type| EVENTABLE_TYPES.each do |eventable_type|
parent_type = eventable_type.parent_class.to_s.underscore parent_type = eventable_type.parent_class.to_s.underscore
......
...@@ -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
......
...@@ -15,8 +15,11 @@ module Gitlab ...@@ -15,8 +15,11 @@ module Gitlab
private_constant :REASONS private_constant :REASONS
<<<<<<< HEAD
prepend ::EE::Gitlab::Ci::Status::Build::Failed prepend ::EE::Gitlab::Ci::Status::Build::Failed
=======
>>>>>>> upstream/master
def status_tooltip def status_tooltip
base_message base_message
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
private
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 end
# def real_size
# If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) @merge_request_diff.real_size
# 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)
......
...@@ -125,7 +125,10 @@ excluded_attributes: ...@@ -125,7 +125,10 @@ excluded_attributes:
- :mirror_overwrites_diverged_branches - :mirror_overwrites_diverged_branches
- :description_html - :description_html
- :repository_languages - :repository_languages
<<<<<<< HEAD
- :packages_enabled - :packages_enabled
=======
>>>>>>> upstream/master
prometheus_metrics: prometheus_metrics:
- :common - :common
- :identifier - :identifier
......
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') # TODO ignoring hashed repositories for now. But revisit to fully support
.chomp('.wiki') # possible orphaned hashed repos
next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX)
# TODO ignoring hashed repositories for now. But revisit to fully support next if Project.find_by_full_path(repo_with_namespace)
# possible orphaned hashed repos
next if repo_with_namespace.start_with?("#{HASHED_REPOSITORY_NAME}/") || Project.find_by_full_path(repo_with_namespace) new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect begin
File.rename(path, new_path) 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
......
...@@ -372,9 +372,12 @@ msgstr "" ...@@ -372,9 +372,12 @@ msgstr ""
msgid "Access expiration date" msgid "Access expiration date"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Access to '%{classification_label}' not allowed" msgid "Access to '%{classification_label}' not allowed"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "" msgstr ""
...@@ -1265,9 +1268,12 @@ msgstr "" ...@@ -1265,9 +1268,12 @@ msgstr ""
msgid "Browse files" msgid "Browse files"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Built-In" msgid "Built-In"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Business metrics (Custom)" msgid "Business metrics (Custom)"
msgstr "" msgstr ""
...@@ -1421,6 +1427,12 @@ msgstr "" ...@@ -1421,6 +1427,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,7 +2214,11 @@ msgstr "" ...@@ -2202,7 +2214,11 @@ msgstr ""
msgid "Contribution guide" msgid "Contribution guide"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Contributions per group member" msgid "Contributions per group member"
=======
msgid "Contributions for <strong>%{calendar_date}</strong>"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Contributors" msgid "Contributors"
...@@ -2672,9 +2688,21 @@ msgstr "" ...@@ -2672,9 +2688,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 +3201,9 @@ msgstr "" ...@@ -3173,6 +3201,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 +3219,9 @@ msgstr "" ...@@ -3188,6 +3219,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 ""
...@@ -3852,12 +3886,15 @@ msgid "GroupsTree|No groups or projects matched your search" ...@@ -3852,12 +3886,15 @@ msgid "GroupsTree|No groups or projects matched your search"
msgstr "" msgstr ""
msgid "GroupsTree|Search by name" msgid "GroupsTree|Search by name"
<<<<<<< HEAD
msgstr "" msgstr ""
msgid "Have your users email" msgid "Have your users email"
msgstr "" msgstr ""
msgid "Header message" msgid "Header message"
=======
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Health Check" msgid "Health Check"
...@@ -4555,12 +4592,15 @@ msgstr "" ...@@ -4555,12 +4592,15 @@ msgstr ""
msgid "Markdown enabled" msgid "Markdown enabled"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Maven Metadata" msgid "Maven Metadata"
msgstr "" msgstr ""
msgid "Maven package" msgid "Maven package"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Max access level" msgid "Max access level"
msgstr "" msgstr ""
...@@ -4825,12 +4865,15 @@ msgstr "" ...@@ -4825,12 +4865,15 @@ msgstr ""
msgid "More" msgid "More"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "More actions" msgid "More actions"
msgstr "" msgstr ""
msgid "More info" msgid "More info"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "More information" msgid "More information"
msgstr "" msgstr ""
...@@ -4980,6 +5023,9 @@ msgstr "" ...@@ -4980,6 +5023,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 +5634,9 @@ msgstr "" ...@@ -5588,6 +5634,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 +5646,30 @@ msgstr "" ...@@ -5597,15 +5646,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 +5682,108 @@ msgstr "" ...@@ -5618,39 +5682,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 +5793,15 @@ msgstr "" ...@@ -5660,6 +5793,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 ""
...@@ -6620,6 +6762,7 @@ msgstr "" ...@@ -6620,6 +6762,7 @@ msgstr ""
msgid "Shared projects" msgid "Shared projects"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero." msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
msgstr "" msgstr ""
...@@ -6629,6 +6772,8 @@ msgstr "" ...@@ -6629,6 +6772,8 @@ msgstr ""
msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes" msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Sherlock Transactions" msgid "Sherlock Transactions"
msgstr "" msgstr ""
...@@ -6991,9 +7136,12 @@ msgstr "" ...@@ -6991,9 +7136,12 @@ msgstr ""
msgid "System Info" msgid "System Info"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "System header and footer:" msgid "System header and footer:"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "System metrics (Custom)" msgid "System metrics (Custom)"
msgstr "" msgstr ""
...@@ -7260,6 +7408,15 @@ msgstr "" ...@@ -7260,6 +7408,15 @@ msgstr ""
msgid "There are no projects shared with this group yet" msgid "There are no projects shared with this group yet"
msgstr "" msgstr ""
<<<<<<< HEAD
=======
msgid "There are no staged changes"
msgstr ""
msgid "There are no unstaged changes"
msgstr ""
>>>>>>> upstream/master
msgid "There are problems accessing Git storage: " msgid "There are problems accessing Git storage: "
msgstr "" msgstr ""
...@@ -7777,10 +7934,14 @@ msgstr "" ...@@ -7777,10 +7934,14 @@ msgstr ""
msgid "Unable to load the diff. %{button_try_again}" msgid "Unable to load the diff. %{button_try_again}"
msgstr "" msgstr ""
<<<<<<< HEAD
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 "Unknown" msgid "Unknown"
=======
msgid "Undo"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "Unlock" msgid "Unlock"
...@@ -7795,6 +7956,9 @@ msgstr "" ...@@ -7795,6 +7956,9 @@ msgstr ""
msgid "Unresolve discussion" msgid "Unresolve discussion"
msgstr "" msgstr ""
msgid "Unstage"
msgstr ""
msgid "Unstage all changes" msgid "Unstage all changes"
msgstr "" msgstr ""
...@@ -7864,9 +8028,6 @@ msgstr "" ...@@ -7864,9 +8028,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 ""
...@@ -7918,9 +8079,6 @@ msgstr "" ...@@ -7918,9 +8079,6 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
msgid "User|Current status"
msgstr ""
msgid "Variables" msgid "Variables"
msgstr "" msgstr ""
...@@ -8278,6 +8436,12 @@ msgstr "" ...@@ -8278,6 +8436,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 ""
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
}, },
"dependencies": { "dependencies": {
"@gitlab-org/gitlab-svgs": "^1.28.0", "@gitlab-org/gitlab-svgs": "^1.29.0",
"@gitlab-org/gitlab-ui": "1.0.5", "@gitlab-org/gitlab-ui": "1.0.5",
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
......
...@@ -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
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment