Commit 031aca80 authored by Felipe Artur's avatar Felipe Artur

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ee-issue_44270

parents ec4105ab dda07ed5
......@@ -10,12 +10,6 @@ engines:
- javascript
exclude_paths:
- "lib/api/v3/*"
eslint:
enabled: true
channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52-1"
ratings:
paths:
- Gemfile.lock
......
......@@ -90,6 +90,11 @@ stages:
- postgres:9.6
- redis:alpine
.use-pg-10-2: &use-pg-10-2
services:
- postgres:10.2
- redis:alpine
.use-pg-with-elasticsearch: &use-pg-with-elasticsearch
services:
- postgres:9.2
......@@ -103,6 +108,18 @@ stages:
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
# END EE-only service helpers
.rails5-variables: &rails5-variables
script:
- export RAILS5=${RAILS5}
- export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
.rails5: &rails5
allow_failure: true
only:
- /rails5/
variables:
BUNDLE_GEMFILE: "Gemfile.rails5"
RAILS5: "true"
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
......@@ -144,6 +161,7 @@ stages:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *rails5-variables
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
......@@ -174,10 +192,18 @@ stages:
<<: *rspec-metadata
<<: *use-pg
.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
<<: *rspec-metadata-pg
<<: *rails5
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
<<: *rspec-metadata-mysql
<<: *rails5
.rspec-metadata-ee: &rspec-metadata-ee
<<: *rspec-metadata
stage: test
......@@ -198,7 +224,7 @@ stages:
<<: *rspec-metadata-ee
<<: *use-mysql-with-elasticsearch
.rspec-geo-pg-9-6: &rspec-metadata-pg-geo
.rspec-geo-pg-9-6: &rspec-metadata-pg-geo-9-6
<<: *rspec-metadata
<<: *use-pg-9-6
stage: test
......@@ -209,10 +235,38 @@ stages:
- scripts/gitaly-test-spawn
- bundle exec rspec --color --format documentation --tag geo ee/spec/
.rspec-geo-pg-10-2: &rspec-metadata-pg-geo-10-2
<<: *rspec-metadata
<<: *use-pg-10-2
stage: test
script:
- export NO_KNAPSACK=1
- export CACHE_CLASSES=true
- source scripts/prepare_postgres_fdw.sh
- scripts/gitaly-test-spawn
- bundle exec rspec --color --format documentation --tag geo ee/spec/
.rspec-ee-pg-rails5: &rspec-ee-pg-rails5
<<: *rspec-ee-pg
<<: *rails5
.rspec-ee-mysql-rails5: &rspec-ee-mysql-rails5
<<: *rspec-ee-mysql
<<: *rails5
.rspec-geo-pg-9-6-rails5: &rspec-metadata-pg-geo-9-6-rails5
<<: *rspec-metadata-pg-geo-9-6
<<: *rails5
.rspec-geo-pg-10-2-rails5: &rspec-metadata-pg-geo-10-2-rails5
<<: *rspec-metadata-pg-geo-10-2
<<: *rails5
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *rails5-variables
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
......@@ -236,10 +290,18 @@ stages:
<<: *spinach-metadata
<<: *use-pg
.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
<<: *spinach-metadata-pg
<<: *rails5
.spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata
<<: *use-mysql
.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
<<: *spinach-metadata-mysql
<<: *rails5
.only-canonical-masters: &only-canonical-masters
only:
- master@gitlab-org/gitlab-ce
......@@ -483,7 +545,15 @@ rspec-pg-ee 0 2: *rspec-ee-pg
rspec-pg-ee 1 2: *rspec-ee-pg
rspec-mysql-ee 0 2: *rspec-ee-mysql
rspec-mysql-ee 1 2: *rspec-ee-mysql
rspec-pg geo: *rspec-metadata-pg-geo
rspec-pg geo 0 2: *rspec-metadata-pg-geo-9-6
rspec-pg geo 1 2: *rspec-metadata-pg-geo-10-2
rspec-pg-ee-rails5 0 2: *rspec-ee-pg-rails5
rspec-pg-ee-rails5 1 2: *rspec-ee-pg-rails5
rspec-mysql-ee-rails5 0 2: *rspec-ee-mysql-rails5
rspec-mysql-ee-rails5 1 2: *rspec-ee-mysql-rails5
rspec-pg-geo-rails5 0 2: *rspec-metadata-pg-geo-9-6-rails5
rspec-pg-geo-rails5 1 2: *rspec-metadata-pg-geo-10-2-rails5
## EE jobs
rspec-pg 0 28: *rspec-metadata-pg
......@@ -550,6 +620,70 @@ spinach-pg 1 2: *spinach-metadata-pg
spinach-mysql 0 2: *spinach-metadata-mysql
spinach-mysql 1 2: *spinach-metadata-mysql
rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5
rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5
rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5
rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5
spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5
spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5
spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5
spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5
static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job
dependencies:
......@@ -722,9 +856,6 @@ codequality:
cache: {}
dependencies: []
script:
# Get the custom rubocop codeclimate image (https://gitlab.com/gitlab-org/codeclimate-rubocop/wikis/home)
- docker pull dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1
- docker tag dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1 codeclimate/codeclimate-rubocop:gitlab-codeclimate-rubocop-0-52-1
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
......@@ -760,7 +891,13 @@ qa:selectors:
- bundle exec bin/qa Test::Sanity::Selectors
coverage:
<<: *dedicated-no-docs-no-db-pull-cache-job
# Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
# download artifacts from all the rspec jobs instead of from setup-test-env only
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
variables:
SETUP_DB: "false"
stage: post-test
script:
- bundle exec scripts/merge-simplecov
......
Please view this file on the master branch, on stable branches it's out of date.
## 10.6.3 (2018-04-03)
- No changes.
## 10.6.2 (2018-03-29)
- No changes.
......@@ -166,6 +170,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Rename "Approve Additionally" to "Add approval".
## 10.5.7 (2018-04-03)
- No changes.
## 10.5.6 (2018-03-16)
- No changes.
......@@ -285,6 +293,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove unaproved typo check in sast:container report.
## 10.4.7 (2018-04-03)
- No changes.
## 10.4.6 (2018-03-16)
- No changes.
......
......@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.6.3 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.6.2 (2018-03-29)
### Fixed (2 changes, 1 of them is from the community)
......@@ -218,6 +226,14 @@ entry.
- Use host URL to build JIRA remote link icon.
## 10.5.7 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.5.6 (2018-03-16)
### Security (2 changes)
......@@ -485,6 +501,14 @@ entry.
- Adds empty state illustration for pending job.
## 10.4.7 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.4.6 (2018-03-16)
### Security (2 changes)
......
......@@ -19,7 +19,7 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
allBoards() {
......
......@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
......@@ -12,7 +11,6 @@ export default {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
},
......@@ -70,9 +68,6 @@ export default {
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return (
this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
);
},
rawDownloadButtonLabel() {
return this.file.binary ? __('Download') : __('Raw');
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="pull-right ide-btn-group"
>
<a
v-tooltip
:href="file.blamePath"
:title="__('Blame')"
class="btn btn-xs btn-transparent blame"
>
<icon
name="blame"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.commitsPath"
:title="__('History')"
class="btn btn-xs btn-transparent history"
>
<icon
name="history"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.permalink"
:title="__('Permalink')"
class="btn btn-xs btn-transparent permalink"
>
<icon
name="link"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.rawPath"
target="_blank"
class="btn btn-xs btn-transparent prepend-left-10 raw"
rel="noopener noreferrer"
:title="rawDownloadButtonLabel">
<icon
name="download"
:size="16"
/>
</a>
</div>
</template>
......@@ -2,10 +2,16 @@
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
export default {
components: {
ContentViewer,
IdeFileButtons,
},
props: {
file: {
type: Object,
......@@ -18,6 +24,16 @@ export default {
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
editTabCSS() {
return {
active: this.file.viewMode === 'edit',
};
},
previewTabCSS() {
return {
active: this.file.viewMode === 'preview',
};
},
},
watch: {
file(oldVal, newVal) {
......@@ -56,6 +72,7 @@ export default {
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
......@@ -153,15 +170,47 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
v-if="shouldHideEditor"
v-html="file.html"
>
class="ide-mode-tabs clearfix"
v-if="!shouldHideEditor">
<ul class="nav-links pull-left">
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'">
{{ __('Edit') }}
</template>
<template v-else>
{{ __('Review') }}
</template>
</a>
</li>
<li
v-if="file.previewMode"
:class="previewTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode:'preview' })">
{{ file.previewMode.previewTitle }}
</a>
</li>
</ul>
<ide-file-buttons
:file="file"
/>
</div>
<div
v-show="!shouldHideEditor"
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
>
</div>
<content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'"
:content="file.content || file.raw"
:path="file.path"
:project-path="file.projectId"/>
</div>
</template>
<script>
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return this.file.rawPath ||
this.file.blamePath ||
this.file.commitsPath ||
this.file.permalink;
},
rawDownloadButtonLabel() {
return this.file.binary ? 'Download' : 'Raw';
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="multi-file-editor-btn-group"
>
<a
:href="file.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
{{ rawDownloadButtonLabel }}
</a>
<div
class="btn-group"
role="group"
aria-label="File actions"
>
<a
:href="file.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="file.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="file.permalink"
class="btn btn-default btn-sm permalink"
>
Permalink
</a>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
components: {
PanelResizer,
export default {
components: {
PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
data() {
return {
width: this.initialWidth,
};
minSize: {
type: Number,
required: false,
default: 340,
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
side: {
type: String,
required: true,
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
data() {
return {
width: this.initialWidth,
};
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
},
methods: {
...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
maxSize: (window.innerWidth / 2),
};
},
maxSize: window.innerWidth / 2,
};
</script>
<template>
......
......@@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
......
......@@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
......
......@@ -42,6 +42,7 @@ export default {
renderError: data.render_error,
raw: null,
baseRaw: null,
html: data.html,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......@@ -83,6 +84,11 @@ export default {
mrChange,
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
Object.assign(state.entries[file.path], {
viewMode,
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
......
......@@ -38,6 +38,8 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
viewMode: 'edit',
previewMode: null,
});
export const decorateData = entity => {
......@@ -57,8 +59,9 @@ export const decorateData = entity => {
changed = false,
parentTreeUrl = '',
base64 = false,
previewMode,
file_lock,
html,
} = entity;
return {
......@@ -79,8 +82,9 @@ export const decorateData = entity => {
renderError,
content,
base64,
previewMode,
file_lock,
html,
};
};
......
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => {
const {
data,
projectId,
branchId,
tempFile = false,
content = '',
base64 = false,
} = e.data;
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
......@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${
parentFolder ? `${parentFolder.path}/` : ''
}${folderName}`;
const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
......@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree',
parentTreeUrl: parentFolder
? parentFolder.url
: `/${projectId}/tree/${branchId}/`,
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
......@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
parentTreeUrl: fileFolder
? fileFolder.url
: `/${projectId}/blob/${branchId}`,
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
previewMode: viewerInformationForPath(blobName),
});
Object.assign(acc, {
......
......@@ -45,7 +45,7 @@
return `#${this.job.runner.id}`;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
......
......@@ -94,10 +94,10 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
......@@ -125,7 +125,6 @@ export default class MilestoneSelect {
return milestone.id;
}
},
isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
......@@ -137,7 +136,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
......@@ -158,6 +157,7 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
......
......@@ -7,8 +7,9 @@ import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import SecurityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue'; // eslint-disable-line import/first
import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue'; // eslint-disable-line import/first
import store from 'ee/vue_shared/security_reports/store'; // eslint-disable-line import/first
Vue.use(Translate);
......@@ -99,54 +100,23 @@ export default () => {
const blobPath = datasetOptions.blobPath;
const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint;
if (endpoint) {
mediator.fetchSastReport(endpoint, blobPath)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.sast.newIssues.length) {
updateBadgeCount(mediator.store.state.securityReports.sast.newIssues.length);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching SAST.'));
});
}
if (dependencyScanningEndpoint) {
mediator.fetchDependencyScanningReport(dependencyScanningEndpoint)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.dependencyScanning.newIssues.length) {
updateBadgeCount(
mediator.store.state.securityReports.dependencyScanning.newIssues.length,
);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching Dependency Scanning.'));
});
}
// Widget summary
// eslint-disable-next-line no-new
new Vue({
el: sastSummary,
store,
components: {
SastSummaryWidget,
},
data() {
return {
mediator,
};
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('sast-summary-widget', {
props: {
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
sastIssues: this.mediator.store.state.securityReports.sast.newIssues.length,
dependencyScanningIssues:
this.mediator.store.state.securityReports.dependencyScanning.newIssues.length,
on: {
updateBadgeCount: this.updateBadge,
},
});
},
......@@ -156,20 +126,24 @@ export default () => {
// eslint-disable-next-line no-new
new Vue({
el: securityTab,
store,
components: {
SecurityReportApp,
},
data() {
return {
mediator,
};
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('security-report-app', {
props: {
securityReports: this.mediator.store.state.securityReports,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
headBlobPath: blobPath,
sastHeadPath: endpoint,
dependencyScanningHeadPath: dependencyScanningEndpoint,
},
on: {
updateBadgeCount: this.updateBadge,
},
});
},
......
......@@ -56,23 +56,4 @@ export default class pipelinesMediator {
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
/**
* EE only
*/
fetchSastReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeSastReport(data, blobPath);
});
}
fetchDependencyScanningReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeDependencyScanningReport(data, blobPath);
});
}
}
import securityState from 'ee/vue_shared/security_reports/helpers/state';
import {
setSastReport,
} from 'ee/vue_shared/security_reports/helpers/utils';
export default class PipelineStore {
constructor() {
this.state = {};
this.state.pipeline = {};
/* EE only */
this.state.securityReports = securityState;
}
storePipeline(pipeline = {}) {
this.state.pipeline = pipeline;
}
/**
* EE only
*/
storeSastReport(data, blobPath) {
Object.assign(
this.state.securityReports.sast,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
storeDependencyScanningReport(data, blobPath) {
Object.assign(
this.state.securityReports.dependencyScanning,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
}
<script>
import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue';
export default {
props: {
content: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
computed: {
viewer() {
const previewInfo = viewerInformationForPath(this.path);
switch (previewInfo.id) {
case 'markdown':
return MarkdownViewer;
default:
return null;
}
},
},
};
</script>
<template>
<div class="preview-container">
<component
:is="viewer"
:project-path="projectPath"
:content="content"
/>
</div>
</template>
const viewers = {
markdown: {
id: 'markdown',
previewTitle: 'Preview Markdown',
},
};
const fileNameViewers = {};
const fileExtensionViewers = {
md: 'markdown',
markdown: 'markdown',
};
export function viewerInformationForPath(path) {
if (!path) return null;
const name = path.split('/').pop();
const viewerName =
fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || '';
return viewers[viewerName];
}
export default viewers;
<script>
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import $ from 'jquery';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
const CancelToken = axios.CancelToken;
let axiosSource;
export default {
components: {
SkeletonLoadingContainer,
},
props: {
content: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
data() {
return {
previewContent: null,
isLoading: false,
};
},
watch: {
content() {
this.previewContent = null;
},
},
created() {
axiosSource = CancelToken.source();
this.fetchMarkdownPreview();
},
updated() {
this.fetchMarkdownPreview();
},
destroyed() {
if (this.isLoading) axiosSource.cancel('Cancelling Preview');
},
methods: {
fetchMarkdownPreview() {
if (this.content && this.previewContent === null) {
this.isLoading = true;
const postBody = {
text: this.content,
};
const postOptions = {
cancelToken: axiosSource.token,
};
axios
.post(
`${gon.relative_url_root}/${this.projectPath}/preview_markdown`,
postBody,
postOptions,
)
.then(({ data }) => {
this.previewContent = data.body;
this.isLoading = false;
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
})
.catch(() => {
this.previewContent = __('An error occurred while fetching markdown preview');
this.isLoading = false;
});
}
},
},
};
</script>
<template>
<div
ref="markdown-preview"
class="md md-previewer">
<skeleton-loading-container v-if="isLoading" />
<div
v-else
v-html="previewContent">
</div>
</div>
</template>
......@@ -28,6 +28,10 @@
border-top: solid 1px $border-color;
}
.mr-widget-border-top {
border-top: 1px solid $border-color;
}
.mr-widget-footer {
padding: 0;
}
......
......@@ -308,14 +308,34 @@
height: 100%;
}
.multi-file-editor-btn-group {
padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
.preview-container {
height: 100%;
overflow: auto;
.md-previewer {
padding: $gl-padding;
}
}
.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
background: $white-light;
.nav-links {
border-bottom: 0;
li a {
padding: $gl-padding-8 $gl-padding;
line-height: $gl-btn-line-height;
}
}
}
.ide-btn-group {
padding: $gl-padding-4 $gl-vert-padding;
}
.ide-status-bar {
border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
......
......@@ -46,7 +46,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids
@existing_oids ||= begin
storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
......
......@@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show
redirect_to project_settings_ci_cd_path(@project, params: params)
end
def update
Projects::UpdateService.new(project, current_user, update_params).tap do |service|
if service.execute
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
run_autodevops_pipeline(service)
redirect_to project_settings_ci_cd_path(@project)
else
render 'show'
end
end
end
private
def run_autodevops_pipeline(service)
return unless service.run_auto_devops_pipeline?
if @project.empty_repo?
flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
return
end
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
end
def update_params
params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
end
......@@ -26,7 +26,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits"
commits_project_graph_path(@project, @id)
when "badges"
project_pipelines_settings_path(@project, ref: @id)
project_settings_ci_cd_path(@project, ref: @id)
else
project_commits_path(@project, @id)
end
......
......@@ -2,13 +2,24 @@ module Projects
module Settings
class CiCdController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
before_action :define_variables
def show
define_runners_variables
define_secret_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
end
def update
Projects::UpdateService.new(project, current_user, update_params).tap do |service|
result = service.execute
if result[:status] == :success
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
run_autodevops_pipeline(service)
redirect_to project_settings_ci_cd_path(@project)
else
render 'show'
end
end
end
def reset_cache
......@@ -25,6 +36,35 @@ module Projects
private
def update_params
params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
def run_autodevops_pipeline(service)
return unless service.run_auto_devops_pipeline?
if @project.empty_repo?
flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
return
end
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
end
def define_variables
define_runners_variables
define_secret_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
end
def define_runners_variables
@project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
......
......@@ -326,7 +326,7 @@ class ProjectsController < Projects::ApplicationController
:avatar,
:build_allow_git_fetch,
:build_coverage_regex,
:build_timeout_in_minutes,
:build_timeout_human_readable,
:resolve_outdated_diff_discussions,
:container_registry_enabled,
:default_branch,
......
module ServicesHelper
prepend EE::ServicesHelper
def service_event_description(event)
case event
when "push", "push_events"
"Event will be triggered by a push to the repository"
when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
when "note", "note_events"
"Event will be triggered when someone adds a comment"
when "confidential_note", "confidential_note_events"
"Event will be triggered when someone adds a comment on a confidential issue"
when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
when "confidential_issue", "confidential_issues_events"
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "pipeline", "pipeline_events"
"Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
......
......@@ -123,7 +123,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree)
return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
......
......@@ -94,6 +94,7 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
......
......@@ -8,14 +8,14 @@ module ChronicDurationAttribute
end
end
def chronic_duration_attr_writer(virtual_attribute, source_attribute)
def chronic_duration_attr_writer(virtual_attribute, source_attribute, parameters = {})
chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method("#{virtual_attribute}=") do |value|
chronic_duration_attributes[virtual_attribute] = value.presence || ''
chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s
begin
new_value = ChronicDuration.parse(value).to_i if value.present?
new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence
assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation
......
......@@ -10,7 +10,7 @@ module Mentionable
DEFAULT_PATTERN = begin
issue_pattern = Issue.reference_pattern
link_patterns = Regexp.union([Issue, Commit, MergeRequest].map(&:link_reference_pattern))
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern))
reference_pattern(link_patterns, issue_pattern)
end
......
......@@ -10,6 +10,7 @@ class ProjectHook < WebHook
:issue_hooks,
:confidential_issue_hooks,
:note_hooks,
:confidential_note_hooks,
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
......
......@@ -276,6 +276,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
def confidential?
noteable.try(:confidential?)
end
def editable?
!system?
end
......@@ -321,6 +325,10 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
def can_create_notification?
true
end
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
......
......@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
include Gitlab::SQL::Pattern
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
# EE specific modules
prepend EE::Project
......@@ -332,6 +333,12 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
message: 'needs to be at least 10 minutes' }
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
......@@ -643,7 +650,7 @@ class Project < ActiveRecord::Base
end
def create_or_update_import_data(data: nil, credentials: nil)
return unless import_url.present? && valid_import_url?
return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
if data
......@@ -1079,6 +1086,16 @@ class Project < ActiveRecord::Base
end
end
# This will return all `lfs_objects` that are accessible to the project.
# So this might be `self.lfs_objects` if the project is not part of a fork
# network, or it is the base of the fork network.
#
# TODO: refactor this to get the correct lfs objects when implementing
# https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
def all_lfs_objects
lfs_storage_project.lfs_objects
end
def personal?
!group
end
......@@ -1313,14 +1330,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_timeout_in_minutes
build_timeout / 60
end
def build_timeout_in_minutes=(value)
self.build_timeout = value.to_i * 60
end
def open_issues_count
Projects::OpenIssuesCountService.new(self).count
end
......@@ -1492,6 +1501,7 @@ class Project < ActiveRecord::Base
remove_import_jid
update_project_counter_caches
after_create_default_branch
refresh_markdown_cache!
end
def update_project_counter_caches
......
......@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end
end
def confidential_issue_channel
properties['confidential_issue_channel'].presence || properties['issue_channel']
end
def confidential_note_channel
properties['confidential_note_channel'].presence || properties['note_channel']
end
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
......@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message
channel_name = get_channel_field(object_kind).presence || channel
event_type = data[:event_type] || object_kind
channel_name = get_channel_field(event_type).presence || channel
opts = {}
opts[:channel] = channel_name if channel_name
......
......@@ -46,7 +46,7 @@ class HipchatService < Service
end
def self.supported_events
%w(push issue confidential_issue merge_request note tag_push pipeline)
%w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
end
def execute(data)
......
......@@ -15,6 +15,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :confidential_note_events, true
default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
......@@ -43,6 +44,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
......@@ -169,8 +171,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
def #{arg}
properties['#{arg}']
unless method_defined?(arg)
def #{arg}
properties['#{arg}']
end
end
def #{arg}=(value)
......
......@@ -166,12 +166,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_save :set_notification_email, if: :email_changed? # in case validation is skipped
before_validation :set_public_email, if: :public_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
......@@ -426,7 +429,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.name = 'Ghost User'
u.notification_email = email
end
end
end
......
......@@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy
delegate { @subject.project }
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed
# to read the actual issue after a more expensive `:read_issue` check.
#
# `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
condition(:visible_to_user, score: 4) do
Project.where(id: @subject.project)
.public_or_visible_to_user(@user)
.with_feature_available_for_user(@subject, @user)
.any?
end
condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
desc "User is the assignee or author"
......
......@@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid
end
class MergeRequestPolicy < IssuablePolicy
prepend EE::MergeRequestPolicy
rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end
......@@ -68,6 +68,22 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
# allowed to read the actual issue after a more expensive `:read_issue`
# check. These checks are intended to be used alongside
# `:read_project_for_iids`.
#
# `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
condition(:issues_visible_to_user, score: 4) do
@subject.feature_available?(:issues, @user)
end
condition(:merge_requests_visible_to_user, score: 4) do
@subject.feature_available?(:merge_requests, @user)
end
features = %w[
merge_requests
issues
......@@ -83,6 +99,10 @@ class ProjectPolicy < BasePolicy
condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) }
end
# `:read_project` may be prevented in EE, but `:read_project_for_iids` should
# not.
rule { guest | admin }.enable :read_project_for_iids
rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access
......@@ -152,6 +172,7 @@ class ProjectPolicy < BasePolicy
# where we enable or prevent it based on other coditions.
rule { (~anonymous & public_project) | internal_access }.policy do
enable :public_user_access
enable :read_project_for_iids
end
rule { can?(:public_user_access) }.policy do
......@@ -257,7 +278,11 @@ class ProjectPolicy < BasePolicy
end
rule { anonymous & ~public_project }.prevent_all
rule { public_project }.enable(:public_access)
rule { public_project }.policy do
enable :public_access
enable :read_project_for_iids
end
rule { can?(:public_access) }.policy do
enable :read_project
......@@ -307,6 +332,14 @@ class ProjectPolicy < BasePolicy
enable :update_pipeline
end
rule do
(can?(:read_project_for_iids) & issues_visible_to_user) | can?(:read_issue)
end.enable :read_issue_iid
rule do
(can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
end.enable :read_merge_request_iid
private
def team_member?
......
class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata|
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_human_readable
expose :timeout_source do |metadata|
metadata.present.timeout_source
end
......
......@@ -42,7 +42,10 @@ module Boards
)
end
attrs[:move_between_ids] = move_between_ids if move_between_ids
if move_between_ids
attrs[:move_between_ids] = move_between_ids
attrs[:board_group_id] = board.group&.id
end
attrs
end
......
......@@ -55,9 +55,10 @@ module Issues
return unless params[:move_between_ids]
after_id, before_id = params.delete(:move_between_ids)
board_group_id = params.delete(:board_group_id)
issue_before = get_issue_if_allowed(before_id) if before_id
issue_after = get_issue_if_allowed(after_id) if after_id
issue_before = get_issue_if_allowed(before_id, board_group_id)
issue_after = get_issue_if_allowed(after_id, board_group_id)
issue.move_between(issue_before, issue_after)
end
......@@ -84,8 +85,16 @@ module Issues
private
def get_issue_if_allowed(id)
issue = Issue.find(id)
def get_issue_if_allowed(id, board_group_id = nil)
return unless id
issue =
if board_group_id
IssuesFinder.new(current_user, group_id: board_group_id).find_by(id: id)
else
project.issues.find(id)
end
issue if can?(current_user, :update_issue, issue)
end
......
......@@ -11,7 +11,7 @@ module Notes
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
return unless @note.for_project_noteable?
return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
......@@ -23,9 +23,13 @@ module Notes
end
def execute_note_hooks
return unless @note.project
note_data = hook_data
@note.project.execute_hooks(note_data, :note_hooks)
@note.project.execute_services(note_data, :note_hooks)
hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
@note.project.execute_hooks(note_data, hooks_scope)
@note.project.execute_services(note_data, hooks_scope)
end
end
end
......@@ -5,8 +5,8 @@ module Projects
class GitlabProjectsImportService
attr_reader :current_user, :params
def initialize(user, params)
@current_user, @params = user, params.dup
def initialize(user, import_params, override_params = nil)
@current_user, @params, @override_params = user, import_params.dup, override_params
end
def execute
......@@ -17,6 +17,7 @@ module Projects
params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path
params[:import_data] = { data: { override_params: @override_params } } if @override_params
::Projects::CreateService.new(current_user, params).execute
end
......
......@@ -28,7 +28,7 @@ module Projects
end
def save_services
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
end
def version_saver
......@@ -55,6 +55,10 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
def lfs_saver
Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
end
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
......
......@@ -429,7 +429,7 @@ module SystemNoteService
def cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner.gfm_reference(noteable.project)
gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
......@@ -687,7 +687,7 @@ module SystemNoteService
text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else
gfm_reference = mentioner.gfm_reference(noteable.project)
gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
text = cross_reference_note_content(gfm_reference)
notes.where(note: [text, text.capitalize])
end
......
......@@ -128,7 +128,7 @@ module ObjectStorage
end
def direct_upload_enabled?
object_store_options.direct_upload
object_store_options&.direct_upload
end
def background_upload_enabled?
......@@ -184,6 +184,14 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
def default_object_store
if self.object_store_enabled? && self.direct_upload_enabled?
Store::REMOTE
else
Store::LOCAL
end
end
end
# allow to configure and overwrite the filename
......@@ -204,12 +212,12 @@ module ObjectStorage
end
def object_store
@object_store ||= model.try(store_serialization_column) || Store::LOCAL
@object_store ||= model.try(store_serialization_column) || self.class.default_object_store
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
@object_store = value || Store::LOCAL
@object_store = value || self.class.default_object_store
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
......
......@@ -22,5 +22,14 @@
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
-# EE-specific start
- if License.feature_available?(:email_additional_text)
.form-group
= f.label :email_additional_text, _('Additional text'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :email_additional_text, class: 'form-control', maxlength: Gitlab::CurrentSettings.email_additional_text_character_limit, rows: 4
.help-block
= _('Add additional text to appear in all email communications. %{character_limit} character limit') % { character_limit: number_with_delimiter(Gitlab::CurrentSettings.email_additional_text_character_limit) }
-# EE-specific end
= f.submit 'Save changes', class: "btn btn-success"
......@@ -2,3 +2,12 @@
Unfortunately, your email message to GitLab could not be processed.
= markdown @reason
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
%p
&mdash;
%br
%br
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end
Unfortunately, your email message to GitLab could not be processed.
\
= @reason
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
\
= '---'
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end
......@@ -63,6 +63,10 @@
%tbody
= yield
-# EE-specific start
= render 'layouts/mailer/additional_text'
-# EE-specific end
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
......
......@@ -2,3 +2,4 @@
---
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
<%# EE-specific start %><%= render 'layouts/mailer/additional_text' %><%# EE-specific end %>
\ No newline at end of file
......@@ -31,3 +31,10 @@
adjust your notification settings.
= email_action @target_url
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
%br
%br
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end
......@@ -10,3 +10,4 @@
<% end -%>
<%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
<%# EE-specific start %><%= render 'layouts/mailer/additional_text' %><%# EE-specific end %>
\ No newline at end of file
......@@ -21,11 +21,11 @@
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%li LFS objects
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
%li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
......
......@@ -27,6 +27,11 @@
window.gl.mrWidgetData.enable_squash_before_merge = '#{@merge_request.project.feature_available?(:merge_request_squash)}' === 'true';
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/project/merge_requests/sast")}';
window.gl.mrWidgetData.sast_container_help_path = '#{help_page_path("user/project/merge_requests/container_scanning")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/project/merge_requests/dast")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/project/merge_requests/dependency_scanning")}';
#js-vue-mr-widget.mr-widget
.content-block.content-block-small.emoji-list-container.js-noteable-awards
......
......@@ -65,4 +65,6 @@
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: expose_sast_data ? sast_artifact_url(@pipeline) : nil,
blob_path: blob_path,
dependency_scanning_endpoint: expose_dependency_data ? dependency_scanning_artifact_url(@pipeline) : nil} }
dependency_scanning_endpoint: expose_dependency_data ? dependency_scanning_artifact_url(@pipeline) : nil,
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning')} }
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
......@@ -73,10 +74,10 @@
%hr
.form-group
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
= f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.help-block
Per job in minutes. If a job passes this threshold, it will be marked as failed
Per job. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
......@@ -151,10 +152,13 @@
%li
excoveralls (Elixir) -
%code \[TOTAL\]\s+(\d+\.\d+)%
%li
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
= f.submit 'Save changes', class: "btn btn-save"
%hr
.row.prepend-top-default
= render partial: 'projects/pipelines_settings/badge', collection: @badges
= render partial: 'badge', collection: @badges
......@@ -3,8 +3,9 @@
- page_title "CI / CD"
- expanded = Rails.env.test?
- general_expanded = @project.errors.empty? ? expanded : true
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
General pipelines settings
......@@ -13,7 +14,7 @@
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content
= render 'projects/pipelines_settings/show'
= render 'form'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
......
......@@ -6,3 +6,12 @@
%p
You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
%p
&mdash;
%br
%br
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end
......@@ -4,3 +4,10 @@ View details: #{admin_projects_url(last_repository_check_failed: 1)}
You are receiving this message because you are a GitLab administrator
for #{Gitlab.config.gitlab.url}.
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
\
= '---'
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end
......@@ -32,6 +32,13 @@
%strong Comments
%p.light
This URL will be triggered when someone adds a comment
%li
= form.check_box :confidential_note_events, class: 'pull-left'
.prepend-left-20
= form.label :confidential_note_events, class: 'list-label' do
%strong Confidential Comments
%p.light
This URL will be triggered when someone adds a comment on a confidential issue
%li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
......
......@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
NotificationService.new.new_note(note)
NotificationService.new.new_note(note) if note.can_create_notification?
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
......
---
title: Fix XSS on diff view stored on filenames
merge_request:
author:
type: security
---
title: Improve performance of loading issues with lots of references to merge requests
merge_request: 17986
author:
type: performance
---
title: Support LFS objects when importing/exporting GitLab project archives
merge_request: 18115
author:
type: added
---
title: Allow overriding params on project import through API
merge_request: 18086
author:
type: added
---
title: Allow to store uploads by default on Object Storage
merge_request:
author:
type: added
---
title: Fix links to subdirectories of a directory with a plus character in its path
merge_request:
author:
type: fixed
---
title: Ensure internal users (ghost, support bot) get assigned a namespace
merge_request:
author:
type: fixed
---
title: Fix 404 in group boards when moving issue between lists
merge_request:
author:
type: fixed
---
title: Adds confidential notes channel for Slack/Mattermost
merge_request:
author:
type: security
---
title: Use human readable value build_timeout in Project
merge_request: 17386
author:
type: changed
---
title: Bulk deleting refs is handled by Gitaly by default
merge_request:
author:
type: performance
---
title: ListCommitsByOid is executed by Gitaly by default
merge_request:
author:
type: performance
......@@ -154,7 +154,7 @@ production: &base
# provider: AWS # Only AWS supported at the moment
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: eu-central-1
# region: us-east-1
## Git LFS
lfs:
......@@ -164,13 +164,14 @@ production: &base
object_store:
enabled: false
remote_directory: lfs-objects # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
region: us-east-1
# Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
......@@ -183,17 +184,18 @@ production: &base
# base_dir: uploads/-/system
object_store:
enabled: false
# remote_directory: uploads # Bucket name
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
# connection:
# provider: AWS
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: eu-central-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
# remote_directory: uploads # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
......
......@@ -412,6 +412,7 @@ Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system
Settings.uploads['object_store'] ||= Settingslogic.new({})
Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
Settings.uploads['object_store']['direct_upload'] = false if Settings.uploads['object_store']['direct_upload'].nil?
Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
Settings.uploads['object_store']['proxy_download'] = false if Settings.uploads['object_store']['proxy_download'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
......
......@@ -473,7 +473,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :settings do
get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
resource :ci_cd, only: [:show], controller: 'ci_cd' do
resource :ci_cd, only: [:show, :update], controller: 'ci_cd' do
post :reset_cache
end
resource :integrations, only: [:show]
......
class AddConfidentialNoteEventsToWebHooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :web_hooks, :confidential_note_events, :boolean
end
def down
remove_column :web_hooks, :confidential_note_events
end
end
class AddConfidentialNoteEventsToServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :services, :confidential_note_events, :boolean
change_column_default :services, :confidential_note_events, true
end
def down
remove_column :services, :confidential_note_events
end
end
class ScheduleSetConfidentialNoteEventsOnWebhooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
INTERVAL = 5.minutes
disable_ddl_transaction!
def up
migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks
migration_name = migration.to_s.demodulize
relation = migration::WebHook.hooks_to_update
queue_background_migration_jobs_by_range_at_intervals(relation,
migration_name,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
end
end
class ScheduleSetConfidentialNoteEventsOnServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
INTERVAL = 20.minutes
disable_ddl_transaction!
def up
migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices
migration_name = migration.to_s.demodulize
relation = migration::Service.services_to_update
queue_background_migration_jobs_by_range_at_intervals(relation,
migration_name,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
end
end
class ScheduleBuildStageMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
##
# This migration has been rescheduled to run again, see
# `20180405101928_reschedule_builds_stages_migration.rb`
#
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
# noop
end
def down
......
class RescheduleBuildsStagesMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
##
# Rescheduled `20180212101928_schedule_build_stage_migration.rb`
#
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180327101207) do
ActiveRecord::Schema.define(version: 20180405101928) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -197,6 +197,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
t.string "email_additional_text"
end
create_table "approvals", force: :cascade do |t|
......@@ -2247,6 +2248,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "confidential_issues_events", default: true, null: false
t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events", default: true
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
......@@ -2608,6 +2610,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "confidential_issues_events", default: false, null: false
t.boolean "repository_update_events", default: false, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events"
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
......
......@@ -55,6 +55,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
- **(Premium)** [Additional email text](../user/admin_area/settings/email.md): Set a custom message that appears at the bottom of every email.
### Maintaining GitLab
......@@ -100,7 +101,7 @@ created in snippets, wikis, and repos.
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
server with IMAP authentication on Ubuntu, to be used with Reply by email.
- **(Premium)** [Additional custom email text](../user/admin_area/settings/email.md): Set a custom message that appears at the bottom of the every email.
- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
[reply by email]: reply_by_email.md
......
......@@ -65,6 +65,7 @@ For source installations the following settings are nested under `uploads:` and
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Uploads will be stored| |
| `direct_upload` | Set to true to enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. This is beta option as it uses inefficient way of uploading data (via Unicorn). The accelerated uploads gonna be implemented in future releases | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
......
......@@ -111,6 +111,9 @@ POST /projects/import
| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace |
| `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
The override params passed will take precendence over all values defined inside the export file.
To upload a file from your filesystem, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
......
......@@ -637,6 +637,7 @@ POST /projects/user/:user_id
| `ci_config_path` | string | no | The path to CI config file |
| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | How many approvers should approve merge request by default |
| `external_authorization_classification_label` | string | no | The classification label for the project |
## Edit project
......@@ -674,6 +675,7 @@ PUT /projects/:id
| `ci_config_path` | string | no | The path to CI config file |
| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | How many approvers should approve merge request by default |
| `external_authorization_classification_label` | string | no | The classification label for the project |
## Fork project
......
......@@ -102,6 +102,7 @@ PUT /application/settings
| `elasticsearch_search` | boolean | no | Enable Elasticsearch search |
| `elasticsearch_url` | string | no | The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., "http://localhost:9200, http://localhost:9201") |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `email_additional_text` | string | no | **(Premium)** Additional text added to the bottom of every email for legal/auditing/compliance reasons reasons |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `geo_status_timeout` | integer | no | The amount of seconds after which a request to get a secondary node status will time out. |
| `gravatar_enabled` | boolean | no | Enable Gravatar |
......
# Email
## Custom logo
The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
## Custom additional text
>[Introduced][ee-5031] in [GitLab Premium][eep] 10.7.
The additional text will appear at the bottom of any email and can be used for
legal/auditing/compliance reasons.
1. Go to **Admin area > Settings** (`/admin/application_settings`).
1. Under the **Email** section, change the **Additional text** field.
1. Hit **Save** for the changes to take effect.
![Admin email settings](img/email_settings.png)
[ee-5031]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5031
[eep]: https://about.gitlab.com/pricing/
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.
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.
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.
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.
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.
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