Commit 2bf83455 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'master' into '42568-pipeline-empty-state'

# Conflicts:
#   app/views/projects/jobs/show.html.haml
#   lib/gitlab/ci/status/core.rb
parents 99edc151 20e9b32c
...@@ -10,12 +10,6 @@ engines: ...@@ -10,12 +10,6 @@ engines:
- javascript - javascript
exclude_paths: exclude_paths:
- "lib/api/v3/*" - "lib/api/v3/*"
eslint:
enabled: true
channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52-1"
ratings: ratings:
paths: paths:
- Gemfile.lock - Gemfile.lock
......
...@@ -78,6 +78,19 @@ stages: ...@@ -78,6 +78,19 @@ stages:
- mysql:latest - mysql:latest
- redis:alpine - redis:alpine
.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/'. # Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes. # Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing # https://docs.gitlab.com/ce/development/writing_documentation.html#testing
...@@ -118,6 +131,7 @@ stages: ...@@ -118,6 +131,7 @@ stages:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *rails5-variables
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -148,14 +162,23 @@ stages: ...@@ -148,14 +162,23 @@ stages:
<<: *rspec-metadata <<: *rspec-metadata
<<: *use-pg <<: *use-pg
.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
<<: *rspec-metadata-pg
<<: *rails5
.rspec-metadata-mysql: &rspec-metadata-mysql .rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata <<: *rspec-metadata
<<: *use-mysql <<: *use-mysql
.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
<<: *rspec-metadata-mysql
<<: *rails5
.spinach-metadata: &spinach-metadata .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *rails5-variables
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -179,10 +202,18 @@ stages: ...@@ -179,10 +202,18 @@ stages:
<<: *spinach-metadata <<: *spinach-metadata
<<: *use-pg <<: *use-pg
.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
<<: *spinach-metadata-pg
<<: *rails5
.spinach-metadata-mysql: &spinach-metadata-mysql .spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata <<: *spinach-metadata
<<: *use-mysql <<: *use-mysql
.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
<<: *spinach-metadata-mysql
<<: *rails5
.only-canonical-masters: &only-canonical-masters .only-canonical-masters: &only-canonical-masters
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
...@@ -468,6 +499,70 @@ spinach-pg 1 2: *spinach-metadata-pg ...@@ -468,6 +499,70 @@ spinach-pg 1 2: *spinach-metadata-pg
spinach-mysql 0 2: *spinach-metadata-mysql spinach-mysql 0 2: *spinach-metadata-mysql
spinach-mysql 1 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: static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
dependencies: dependencies:
...@@ -632,9 +727,6 @@ codequality: ...@@ -632,9 +727,6 @@ codequality:
cache: {} cache: {}
dependencies: [] dependencies: []
script: 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 # 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/') - 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 - 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
...@@ -670,7 +762,13 @@ qa:selectors: ...@@ -670,7 +762,13 @@ qa:selectors:
- bundle exec bin/qa Test::Sanity::Selectors - bundle exec bin/qa Test::Sanity::Selectors
coverage: 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 stage: post-test
script: script:
- bundle exec scripts/merge-simplecov - bundle exec scripts/merge-simplecov
......
...@@ -2,6 +2,14 @@ ...@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 10.6.2 (2018-03-29)
### Fixed (2 changes, 1 of them is from the community) ### Fixed (2 changes, 1 of them is from the community)
...@@ -217,6 +225,14 @@ entry. ...@@ -217,6 +225,14 @@ entry.
- Use host URL to build JIRA remote link icon. - 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) ## 10.5.6 (2018-03-16)
### Security (2 changes) ### Security (2 changes)
...@@ -484,6 +500,14 @@ entry. ...@@ -484,6 +500,14 @@ entry.
- Adds empty state illustration for pending job. - 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) ## 10.4.6 (2018-03-16)
### Security (2 changes) ### Security (2 changes)
......
...@@ -19,7 +19,7 @@ export default class BoardService { ...@@ -19,7 +19,7 @@ export default class BoardService {
} }
static generateIssuePath(boardId, id) { 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}` : ''}`;
} }
all() { all() {
......
...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager { ...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor; this.includeAncestorGroups = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent; this.includeDescendantGroups = isGroupDecendent;
this.setupMapping(); this.setupMapping();
...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager { ...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
} }
getLabelsEndpoint() { getLabelsEndpoint() {
const endpoint = `${this.baseEndpoint}/labels.json`; let endpoint = `${this.baseEndpoint}/labels.json?`;
if (this.groupsOnly) {
endpoint = `${endpoint}only_group_labels=true&`;
}
if (this.includeAncestorGroups) {
endpoint = `${endpoint}include_ancestor_groups=true&`;
}
if (this.includeDescendantGroups) {
endpoint = `${endpoint}include_descendant_groups=true`;
}
return endpoint; return endpoint;
} }
......
...@@ -21,7 +21,7 @@ export default class FilteredSearchManager { ...@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false, isGroup = false,
isGroupAncestor = false, isGroupAncestor = true,
isGroupDecendent = false, isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
...@@ -86,6 +86,7 @@ export default class FilteredSearchManager { ...@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
page: this.page, page: this.page,
isGroup: this.isGroup, isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor, isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys, filteredSearchTokenKeys: this.filteredSearchTokenKeys,
}); });
......
...@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex'; ...@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
...@@ -12,7 +11,6 @@ export default { ...@@ -12,7 +11,6 @@ export default {
ideSidebar, ideSidebar,
ideContextbar, ideContextbar,
repoTabs, repoTabs,
repoFileButtons,
ideStatusBar, ideStatusBar,
repoEditor, repoEditor,
}, },
...@@ -70,9 +68,6 @@ export default { ...@@ -70,9 +68,6 @@ export default {
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
:file="activeFile" :file="activeFile"
/> />
<repo-file-buttons
:file="activeFile"
/>
<ide-status-bar <ide-status-bar
:file="activeFile" :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 @@ ...@@ -2,10 +2,16 @@
/* global monaco */ /* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
export default { export default {
components: {
ContentViewer,
IdeFileButtons,
},
props: { props: {
file: { file: {
type: Object, type: Object,
...@@ -13,11 +19,21 @@ export default { ...@@ -13,11 +19,21 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']), ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']), ...mapGetters(['currentMergeRequest']),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw; return this.file && this.file.binary && !this.file.raw;
}, },
editTabCSS() {
return {
active: this.file.viewMode === 'edit',
};
},
previewTabCSS() {
return {
active: this.file.viewMode === 'preview',
};
},
}, },
watch: { watch: {
file(oldVal, newVal) { file(oldVal, newVal) {
...@@ -26,15 +42,17 @@ export default { ...@@ -26,15 +42,17 @@ export default {
this.initMonaco(); this.initMonaco();
} }
}, },
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() { rightPanelCollapsed() {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
viewer() { viewer() {
this.createEditorInstance(); this.createEditorInstance();
}, },
panelResizing() {
if (!this.panelResizing) {
this.editor.updateDimensions();
}
},
}, },
beforeDestroy() { beforeDestroy() {
this.editor.dispose(); this.editor.dispose();
...@@ -56,6 +74,7 @@ export default { ...@@ -56,6 +74,7 @@ export default {
'changeFileContent', 'changeFileContent',
'setFileLanguage', 'setFileLanguage',
'setEditorPosition', 'setEditorPosition',
'setFileViewMode',
'setFileEOL', 'setFileEOL',
'updateViewer', 'updateViewer',
'updateDelayViewerUpdated', 'updateDelayViewerUpdated',
...@@ -153,15 +172,47 @@ export default { ...@@ -153,15 +172,47 @@ export default {
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div
v-if="shouldHideEditor" class="ide-mode-tabs clearfix"
v-html="file.html" 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>
<div <div
v-show="!shouldHideEditor" v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor" ref="editor"
class="multi-file-editor-holder" class="multi-file-editor-holder"
> >
</div> </div>
<content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'"
:content="file.content || file.raw"
:path="file.path"
:project-path="file.projectId"/>
</div> </div>
</template> </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> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default { export default {
components: { components: {
PanelResizer, PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
}, },
props: { initialWidth: {
collapsible: { type: Number,
type: Boolean, required: true,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
}, },
data() { minSize: {
return { type: Number,
width: this.initialWidth, required: false,
}; default: 340,
}, },
computed: { side: {
...mapState({ type: String,
collapsed(state) { required: true,
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
}, },
methods: { },
...mapActions([ data() {
'setPanelCollapsedStatus', return {
'setResizingStatus', width: this.initialWidth,
]), };
toggleFullbarCollapsed() { },
if (this.collapsed && this.collapsible) { computed: {
this.setPanelCollapsedStatus({ ...mapState({
side: this.side, collapsed(state) {
collapsed: !this.collapsed, 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> </script>
<template> <template>
......
...@@ -69,6 +69,7 @@ export default class Editor { ...@@ -69,6 +69,7 @@ export default class Editor {
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: 'none', renderLineHighlight: 'none',
hideCursorInOverviewRuler: true, hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
})), })),
); );
...@@ -81,7 +82,7 @@ export default class Editor { ...@@ -81,7 +82,7 @@ export default class Editor {
} }
attachModel(model) { attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') { if (this.isDiffEditorType) {
this.instance.setModel({ this.instance.setModel({
original: model.getOriginalModel(), original: model.getOriginalModel(),
modified: model.getModel(), modified: model.getModel(),
...@@ -153,6 +154,7 @@ export default class Editor { ...@@ -153,6 +154,7 @@ export default class Editor {
updateDimensions() { updateDimensions() {
this.instance.layout(); this.instance.layout();
this.updateDiffView();
} }
setPosition({ lineNumber, column }) { setPosition({ lineNumber, column }) {
...@@ -171,4 +173,20 @@ export default class Editor { ...@@ -171,4 +173,20 @@ export default class Editor {
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e))); this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
} }
updateDiffView() {
if (!this.isDiffEditorType) return;
this.instance.updateOptions({
renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
});
}
get isDiffEditorType() {
return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
}
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
} }
...@@ -6,7 +6,7 @@ export const defaultEditorOptions = { ...@@ -6,7 +6,7 @@ export const defaultEditorOptions = {
minimap: { minimap: {
enabled: false, enabled: false,
}, },
wordWrap: 'bounded', wordWrap: 'on',
}; };
export default [ export default [
......
...@@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn ...@@ -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) => { export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path]; const file = state.entries[path];
......
...@@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA'; ...@@ -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 UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION'; 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 SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
......
...@@ -42,6 +42,7 @@ export default { ...@@ -42,6 +42,7 @@ export default {
renderError: data.render_error, renderError: data.render_error,
raw: null, raw: null,
baseRaw: null, baseRaw: null,
html: data.html,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
...@@ -83,6 +84,11 @@ export default { ...@@ -83,6 +84,11 @@ export default {
mrChange, mrChange,
}); });
}, },
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
Object.assign(state.entries[file.path], {
viewMode,
});
},
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: state.entries[path].raw, content: state.entries[path].raw,
......
...@@ -38,6 +38,8 @@ export const dataStructure = () => ({ ...@@ -38,6 +38,8 @@ export const dataStructure = () => ({
editorColumn: 1, editorColumn: 1,
fileLanguage: '', fileLanguage: '',
eol: '', eol: '',
viewMode: 'edit',
previewMode: null,
}); });
export const decorateData = entity => { export const decorateData = entity => {
...@@ -57,8 +59,9 @@ export const decorateData = entity => { ...@@ -57,8 +59,9 @@ export const decorateData = entity => {
changed = false, changed = false,
parentTreeUrl = '', parentTreeUrl = '',
base64 = false, base64 = false,
previewMode,
file_lock, file_lock,
html,
} = entity; } = entity;
return { return {
...@@ -79,8 +82,9 @@ export const decorateData = entity => { ...@@ -79,8 +82,9 @@ export const decorateData = entity => {
renderError, renderError,
content, content,
base64, base64,
previewMode,
file_lock, file_lock,
html,
}; };
}; };
......
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils'; import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => { self.addEventListener('message', e => {
const { const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
data,
projectId,
branchId,
tempFile = false,
content = '',
base64 = false,
} = e.data;
const treeList = []; const treeList = [];
let file; let file;
...@@ -19,9 +13,7 @@ self.addEventListener('message', e => { ...@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) { if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => { pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]]; const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${ const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
parentFolder ? `${parentFolder.path}/` : ''
}${folderName}`;
const foundEntry = acc[folderPath]; const foundEntry = acc[folderPath];
if (!foundEntry) { if (!foundEntry) {
...@@ -33,9 +25,7 @@ self.addEventListener('message', e => { ...@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath, path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`, url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree', type: 'tree',
parentTreeUrl: parentFolder parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
? parentFolder.url
: `/${projectId}/tree/${branchId}/`,
tempFile, tempFile,
changed: tempFile, changed: tempFile,
opened: tempFile, opened: tempFile,
...@@ -70,13 +60,12 @@ self.addEventListener('message', e => { ...@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path, path,
url: `/${projectId}/blob/${branchId}/${path}`, url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob', type: 'blob',
parentTreeUrl: fileFolder parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
? fileFolder.url
: `/${projectId}/blob/${branchId}`,
tempFile, tempFile,
changed: tempFile, changed: tempFile,
content, content,
base64, base64,
previewMode: viewerInformationForPath(blobName),
}); });
Object.assign(acc, { Object.assign(acc, {
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
hasTimeout() { 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() { timeout() {
if (this.job.metadata == null) { if (this.job.metadata == null) {
......
...@@ -94,10 +94,10 @@ export default class MilestoneSelect { ...@@ -94,10 +94,10 @@ export default class MilestoneSelect {
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}), }),
renderRow: milestone => ` renderRow: milestone => `
<li data-milestone-id="${milestone.name}"> <li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'> <a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)} ${_.escape(milestone.title)}
</a> </a>
...@@ -125,7 +125,6 @@ export default class MilestoneSelect { ...@@ -125,7 +125,6 @@ export default class MilestoneSelect {
return milestone.id; return milestone.id;
} }
}, },
isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => { hidden: () => {
$selectBox.hide(); $selectBox.hide();
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
...@@ -137,7 +136,7 @@ export default class MilestoneSelect { ...@@ -137,7 +136,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
} }
$('a.is-active', $el).removeClass('is-active'); $('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'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => { clicked: (clickEvent) => {
...@@ -158,6 +157,7 @@ export default class MilestoneSelect { ...@@ -158,6 +157,7 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index'); const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone); const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault(); e.preventDefault();
return; return;
......
...@@ -1190,12 +1190,12 @@ export default class Notes { ...@@ -1190,12 +1190,12 @@ export default class Notes {
addForm = false; addForm = false;
let lineTypeSelector = ''; let lineTypeSelector = '';
rowCssToAdd = rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>';
// In parallel view, look inside the correct left/right pane // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) { if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`; lineTypeSelector = `.${lineType}`;
rowCssToAdd = rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>';
} }
const notesContentSelector = `.notes_content${lineTypeSelector} .content`; const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector); let notesContent = targetRow.find(notesContentSelector);
......
...@@ -258,9 +258,7 @@ Please check your network connection and try again.`; ...@@ -258,9 +258,7 @@ Please check your network connection and try again.`;
:key="note.id" :key="note.id"
/> />
</ul> </ul>
<div <div class="discussion-reply-holder">
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder">
<template v-if="!isReplying && canReply"> <template v-if="!isReplying && canReply">
<div <div
class="btn-group-justified discussion-with-resolve-btn" class="btn-group-justified discussion-with-resolve-btn"
......
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.ISSUES, page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS, page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
* "tooltip": "passed",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "retry", * "icon": "retry",
...@@ -69,12 +70,12 @@ ...@@ -69,12 +70,12 @@
textBuilder.push(this.job.name); textBuilder.push(this.job.name);
} }
if (this.job.name && this.status.label) { if (this.job.name && this.status.tooltip) {
textBuilder.push('-'); textBuilder.push('-');
} }
if (this.status.label) { if (this.status.tooltip) {
textBuilder.push(`${this.job.status.label}`); textBuilder.push(`${this.job.status.tooltip}`);
} }
return textBuilder.join(' '); return textBuilder.join(' ');
...@@ -100,6 +101,7 @@ ...@@ -100,6 +101,7 @@
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-container="body" data-container="body"
data-html="true"
class="js-pipeline-graph-job-link" class="js-pipeline-graph-job-link"
> >
...@@ -115,6 +117,7 @@ ...@@ -115,6 +117,7 @@
class="js-job-component-tooltip" class="js-job-component-tooltip"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-html="true"
data-container="body" data-container="body"
> >
......
<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>
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
color: $list-text-disabled-color; color: $list-text-disabled-color;
} }
&:not(.ui-sort-disabled):hover {
background: $row-hover;
}
&.unstyled { &.unstyled {
&:hover { &:hover {
background: none; background: none;
...@@ -34,14 +38,15 @@ ...@@ -34,14 +38,15 @@
background-color: $list-warning-row-bg; background-color: $list-warning-row-bg;
border-color: $list-warning-row-border; border-color: $list-warning-row-border;
color: $list-warning-row-color; color: $list-warning-row-color;
}
&.smoke { background-color: $gray-light; } &:hover {
background: $list-warning-row-bg;
}
&:not(.ui-sort-disabled):hover {
background: $row-hover;
} }
&.smoke { background-color: $gray-light; }
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
......
...@@ -289,6 +289,11 @@ body { ...@@ -289,6 +289,11 @@ body {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
&.with-button {
line-height: 34px;
}
} }
.page-title-empty { .page-title-empty {
......
...@@ -391,7 +391,7 @@ ...@@ -391,7 +391,7 @@
} }
&:hover { &:hover {
background-color: $row-hover; background-color: $dropdown-item-hover-bg;
} }
.icon-retry { .icon-retry {
......
...@@ -813,6 +813,7 @@ ...@@ -813,6 +813,7 @@
} }
.discussion-notes { .discussion-notes {
padding: 0 $gl-padding $gl-padding;
min-height: 35px; min-height: 35px;
&:first-child { &:first-child {
......
...@@ -173,11 +173,7 @@ ...@@ -173,11 +173,7 @@
} }
.discussion-form { .discussion-form {
background-color: $white-light; padding-top: $gl-padding-top;
}
.discussion-form-container {
padding: $gl-padding-top $gl-padding $gl-padding;
} }
.discussion-notes .disabled-comment { .discussion-notes .disabled-comment {
...@@ -237,12 +233,7 @@ ...@@ -237,12 +233,7 @@
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.discussion-reply-holder { .discussion-reply-holder {
background-color: $white-light; padding-top: $gl-padding;
padding: 10px 16px;
&.is-replying {
padding-bottom: $gl-padding;
}
} }
} }
......
...@@ -47,7 +47,7 @@ ul.notes { ...@@ -47,7 +47,7 @@ ul.notes {
} }
.timeline-entry-inner { .timeline-entry-inner {
padding: $gl-padding $gl-btn-padding; padding: $gl-padding 0;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
} }
...@@ -94,12 +94,6 @@ ul.notes { ...@@ -94,12 +94,6 @@ ul.notes {
} }
} }
&.note-discussion {
.timeline-entry-inner {
padding: $gl-padding 10px;
}
}
.editing-spinner { .editing-spinner {
display: none; display: none;
} }
...@@ -352,6 +346,8 @@ ul.notes { ...@@ -352,6 +346,8 @@ ul.notes {
} }
.discussion-notes { .discussion-notes {
background-color: $white-light;
&:not(:first-child) { &:not(:first-child) {
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
margin-top: 20px; margin-top: 20px;
...@@ -363,10 +359,6 @@ ul.notes { ...@@ -363,10 +359,6 @@ ul.notes {
} }
} }
.notes {
background-color: $white-light;
}
a code { a code {
top: 0; top: 0;
margin-right: 0; margin-right: 0;
...@@ -647,8 +639,6 @@ ul.notes { ...@@ -647,8 +639,6 @@ ul.notes {
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
.timeline-entry-inner { .timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
border-bottom: 0; border-bottom: 0;
} }
} }
......
.pages-domain-list {
&-item {
position: relative;
display: flex;
align-items: center;
.domain-status {
display: inline-flex;
left: $gl-padding;
position: absolute;
}
.domain-name {
flex-grow: 1;
}
}
&.has-verification-status > li {
padding-left: 3 * $gl-padding;
}
}
.status-badge {
display: inline-flex;
margin-bottom: $gl-padding-8;
// Most of the following settings "stolen" from btn-sm
// Border radius is overwritten for both
.label,
.btn {
padding: $gl-padding-4 $gl-padding-8;
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
border-radius: 0;
display: flex;
align-items: center;
}
.btn svg {
top: auto;
}
:first-child {
border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
}
:not(:first-child) {
border-left: 0;
}
:last-child {
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
}
}
...@@ -210,13 +210,8 @@ ...@@ -210,13 +210,8 @@
} }
.created-personal-access-token-container { .created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
display: inline;
}
.btn-clipboard { .btn-clipboard {
margin-left: 5px; border: 1px solid $border-color;
} }
} }
......
...@@ -308,14 +308,34 @@ ...@@ -308,14 +308,34 @@
height: 100%; height: 100%;
} }
.multi-file-editor-btn-group { .preview-container {
padding: $gl-bar-padding $gl-padding; height: 100%;
border-top: 1px solid $white-dark; overflow: auto;
.md-previewer {
padding: $gl-padding;
}
}
.ide-mode-tabs {
border-bottom: 1px solid $white-dark; 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 { .ide-status-bar {
border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding; padding: $gl-bar-padding $gl-padding;
background: $white-light; background: $white-light;
display: flex; display: flex;
......
...@@ -4,8 +4,8 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -4,8 +4,8 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :check_merge_requests_available! before_action :check_merge_requests_available!
before_action :merge_request before_action :merge_request
before_action :discussion before_action :discussion, only: [:resolve, :unresolve]
before_action :authorize_resolve_discussion! before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
def resolve def resolve
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
......
...@@ -7,6 +7,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
attr_reader :authentication_result, :redirected_path attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
alias_method :user, :actor alias_method :user, :actor
alias_method :authenticated_user, :actor alias_method :authenticated_user, :actor
......
...@@ -64,7 +64,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -64,7 +64,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
@access ||= access_klass.new(access_actor, project, @access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities, 'http', authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id], project_path: project_path, namespace_path: params[:namespace_id], project_path: project_path,
redirected_path: redirected_path) redirected_path: redirected_path, auth_result_type: auth_result_type)
end end
def access_actor def access_actor
......
...@@ -2,7 +2,6 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -2,7 +2,6 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload include SendFileUpload
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace] only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, before_action :authorize_update_build!,
...@@ -45,8 +44,11 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -45,8 +44,11 @@ class Projects::JobsController < Projects::ApplicationController
end end
def show def show
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') @builds = @project.pipelines
@builds = @builds.where("id not in (?)", @build.id) .find_by_sha(@build.sha)
.builds
.order('id DESC')
.present(current_user: current_user)
@pipeline = @build.pipeline @pipeline = @build.pipeline
respond_to do |format| respond_to do |format|
...@@ -128,7 +130,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -128,7 +130,7 @@ class Projects::JobsController < Projects::ApplicationController
if stream.file? if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else else
render_404 send_data stream.raw, type: 'text/plain; charset=utf-8', disposition: 'inline', filename: 'job.log'
end end
end end
end end
......
...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def find_labels def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
...@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids def existing_oids
@existing_oids ||= begin @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
end end
......
...@@ -31,7 +31,9 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -31,7 +31,9 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
render plain: 'Unprocessable entity', status: 422 render plain: 'Unprocessable entity', status: 422
end end
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render_400 render_lfs_forbidden
rescue UploadedFile::InvalidPathError
render_lfs_forbidden
rescue ObjectStorage::RemoteStoreError rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden render_lfs_forbidden
end end
...@@ -66,10 +68,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -66,10 +68,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end end
def create_file!(oid, size) def create_file!(oid, size)
LfsObject.new(oid: oid, size: size).tap do |object| uploaded_file = UploadedFile.from_params(
object.file.store_workhorse_file!(params, :file) params, :file, LfsObjectUploader.workhorse_local_upload_path)
object.save! return unless uploaded_file
end
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end end
def link_to_project!(object) def link_to_project!(object)
......
...@@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show def show
redirect_to project_settings_ci_cd_path(@project, params: params) redirect_to project_settings_ci_cd_path(@project, params: params)
end 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 end
...@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits" when "graphs_commits"
commits_project_graph_path(@project, @id) commits_project_graph_path(@project, @id)
when "badges" when "badges"
project_pipelines_settings_path(@project, ref: @id) project_settings_ci_cd_path(@project, ref: @id)
else else
project_commits_path(@project, @id) project_commits_path(@project, @id)
end end
......
...@@ -2,13 +2,24 @@ module Projects ...@@ -2,13 +2,24 @@ module Projects
module Settings module Settings
class CiCdController < Projects::ApplicationController class CiCdController < Projects::ApplicationController
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
before_action :define_variables
def show def show
define_runners_variables end
define_secret_variables
define_triggers_variables def update
define_badges_variables Projects::UpdateService.new(project, current_user, update_params).tap do |service|
define_auto_devops_variables 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 end
def reset_cache def reset_cache
...@@ -25,6 +36,35 @@ module Projects ...@@ -25,6 +36,35 @@ module Projects
private 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 def define_runners_variables
@project_runners = @project.runners.ordered @project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners @assignable_runners = current_user.ci_authorized_runners
......
...@@ -324,7 +324,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -324,7 +324,7 @@ class ProjectsController < Projects::ApplicationController
:avatar, :avatar,
:build_allow_git_fetch, :build_allow_git_fetch,
:build_coverage_regex, :build_coverage_regex,
:build_timeout_in_minutes, :build_timeout_human_readable,
:resolve_outdated_diff_discussions, :resolve_outdated_diff_discussions,
:container_registry_enabled, :container_registry_enabled,
:default_branch, :default_branch,
......
...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder ...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
if project if project
if project.group.present? if project.group.present?
labels_table = Label.arel_table labels_table = Label.arel_table
group_ids = group_ids_for(project.group)
label_ids << Label.where( label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
) )
) )
...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder ...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
label_ids << project.labels label_ids << project.labels
end end
end end
elsif only_group_labels?
label_ids << Label.where(group_id: group_ids)
else else
if group?
group = Group.find(params[:group_id])
label_ids << Label.where(group_id: group_ids_for(group))
end
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
end end
label_ids label_ids
...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder ...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group_ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group.
def group_ids_for(group)
strong_memoize(:group_ids) do strong_memoize(:group_ids) do
groups_user_can_read_labels(groups_to_include).map(&:id) groups = groups_to_include(group)
groups_user_can_read_labels(groups).map(&:id)
end end
end end
def groups_to_include def groups_to_include(group)
group = Group.find(params[:group_id])
groups = [group] groups = [group]
groups += group.ancestors if params[:include_ancestor_groups].present? groups += group.ancestors if include_ancestor_groups?
groups += group.descendants if params[:include_descendant_groups].present? groups += group.descendants if include_descendant_groups?
groups groups
end end
def include_ancestor_groups?
params[:include_ancestor_groups]
end
def include_descendant_groups?
params[:include_descendant_groups]
end
def group? def group?
params[:group_id].present? params[:group_id].present?
end end
......
...@@ -53,10 +53,12 @@ module BoardsHelper ...@@ -53,10 +53,12 @@ module BoardsHelper
end end
def board_list_data def board_list_data
include_descendant_groups = @group&.present?
{ {
toggle: "dropdown", toggle: "dropdown",
list_labels_path: labels_filter_path(true), list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
labels: labels_filter_path(true), labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint, labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project&.path, project_path: @project&.path,
......
...@@ -129,13 +129,17 @@ module LabelsHelper ...@@ -129,13 +129,17 @@ module LabelsHelper
end end
end end
def labels_filter_path(only_group_labels = false) def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project project = @target_project || @project
options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
if project if project
project_labels_path(project, :json) project_labels_path(project, :json, options)
elsif @group elsif @group
options = { only_group_labels: only_group_labels } if only_group_labels options[:only_group_labels] = only_group_labels if only_group_labels
group_labels_path(@group, :json, options) group_labels_path(@group, :json, options)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
......
module ServicesHelper module 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) def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events" "#{event}_events"
......
...@@ -123,7 +123,7 @@ module TreeHelper ...@@ -123,7 +123,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant # returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree) 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) subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir? if subtree.count == 1 && subtree.first.dir?
......
...@@ -16,7 +16,7 @@ class Appearance < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Appearance < ActiveRecord::Base
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = 'current_appearance'.freeze CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
after_commit :flush_redis_cache after_commit :flush_redis_cache
......
...@@ -90,6 +90,7 @@ module Ci ...@@ -90,6 +90,7 @@ module Ci
before_save :ensure_token before_save :ensure_token
before_destroy { unscoped_project } before_destroy { unscoped_project }
before_create :ensure_metadata
after_create unless: :importing? do |build| after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) } run_after_commit { BuildHooksWorker.perform_async(build.id) }
end end
......
...@@ -7,6 +7,7 @@ module Ci ...@@ -7,6 +7,7 @@ module Ci
belongs_to :project belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
before_save :update_file_store
before_save :set_size, if: :file_changed? before_save :set_size, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
...@@ -21,6 +22,10 @@ module Ci ...@@ -21,6 +22,10 @@ module Ci
trace: 3 trace: 3
} }
def update_file_store
self.file_store = file.object_store
end
def self.artifacts_size_for(project) def self.artifacts_size_for(project)
self.where(project: project).sum(:size) self.where(project: project).sum(:size)
end end
......
...@@ -32,7 +32,8 @@ class Commit ...@@ -32,7 +32,8 @@ class Commit
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field) def banzai_render_context(field)
context = { pipeline: :single_line, project: self.project } pipeline = field == :description ? :commit_description : :single_line
context = { pipeline: pipeline, project: self.project }
context[:author] = self.author if self.author context[:author] = self.author if self.author
context context
......
...@@ -8,14 +8,14 @@ module ChronicDurationAttribute ...@@ -8,14 +8,14 @@ module ChronicDurationAttribute
end end
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) chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method("#{virtual_attribute}=") do |value| 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 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) assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation # ignore error as it will be caught by validation
......
module Presentable module Presentable
extend ActiveSupport::Concern
class_methods do
def present(attributes)
all.map { |klass_object| klass_object.present(attributes) }
end
end
def present(**attributes) def present(**attributes)
Gitlab::View::Presenter::Factory Gitlab::View::Presenter::Factory
.new(self, attributes) .new(self, attributes)
......
...@@ -7,6 +7,7 @@ class ProjectHook < WebHook ...@@ -7,6 +7,7 @@ class ProjectHook < WebHook
:issue_hooks, :issue_hooks,
:confidential_issue_hooks, :confidential_issue_hooks,
:note_hooks, :note_hooks,
:confidential_note_hooks,
:merge_request_hooks, :merge_request_hooks,
:job_hooks, :job_hooks,
:pipeline_hooks, :pipeline_hooks,
......
...@@ -268,6 +268,10 @@ class Note < ActiveRecord::Base ...@@ -268,6 +268,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end end
def confidential?
noteable.try(:confidential?)
end
def editable? def editable?
!system? !system?
end end
...@@ -313,6 +317,10 @@ class Note < ActiveRecord::Base ...@@ -313,6 +317,10 @@ class Note < ActiveRecord::Base
!system? && !for_snippet? !system? && !for_snippet?
end end
def can_create_notification?
true
end
def discussion_class(noteable = nil) def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are # When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually. # displayed in one discussion instead of individually.
......
...@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base ...@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include DeploymentPlatform include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
...@@ -325,6 +326,12 @@ class Project < ActiveRecord::Base ...@@ -325,6 +326,12 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } 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 # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
def self.public_or_visible_to_user(user = nil) def self.public_or_visible_to_user(user = nil)
...@@ -630,7 +637,7 @@ class Project < ActiveRecord::Base ...@@ -630,7 +637,7 @@ class Project < ActiveRecord::Base
end end
def create_or_update_import_data(data: nil, credentials: nil) 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 project_import_data = import_data || build_import_data
if data if data
...@@ -1066,6 +1073,16 @@ class Project < ActiveRecord::Base ...@@ -1066,6 +1073,16 @@ class Project < ActiveRecord::Base
end end
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? def personal?
!group !group
end end
...@@ -1299,14 +1316,6 @@ class Project < ActiveRecord::Base ...@@ -1299,14 +1316,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end 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 def open_issues_count
Projects::OpenIssuesCountService.new(self).count Projects::OpenIssuesCountService.new(self).count
end end
...@@ -1478,6 +1487,7 @@ class Project < ActiveRecord::Base ...@@ -1478,6 +1487,7 @@ class Project < ActiveRecord::Base
remove_import_jid remove_import_jid
update_project_counter_caches update_project_counter_caches
after_create_default_branch after_create_default_branch
refresh_markdown_cache!
end end
def update_project_counter_caches def update_project_counter_caches
......
...@@ -21,8 +21,16 @@ class ChatNotificationService < Service ...@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end end
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 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] pipeline wiki_page]
end end
...@@ -55,7 +63,9 @@ class ChatNotificationService < Service ...@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message 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 = {}
opts[:channel] = channel_name if channel_name opts[:channel] = channel_name if channel_name
......
...@@ -46,7 +46,7 @@ class HipchatService < Service ...@@ -46,7 +46,7 @@ class HipchatService < Service
end end
def self.supported_events 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 end
def execute(data) def execute(data)
......
...@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base ...@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true default_value_for :tag_push_events, true
default_value_for :note_events, true default_value_for :note_events, true
default_value_for :confidential_note_events, true
default_value_for :job_events, true default_value_for :job_events, true
default_value_for :pipeline_events, true default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true default_value_for :wiki_page_events, true
...@@ -42,6 +43,7 @@ class Service < ActiveRecord::Base ...@@ -42,6 +43,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) } scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_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 :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 :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
...@@ -168,8 +170,10 @@ class Service < ActiveRecord::Base ...@@ -168,8 +170,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args) def self.prop_accessor(*args)
args.each do |arg| args.each do |arg|
class_eval %{ class_eval %{
def #{arg} unless method_defined?(arg)
properties['#{arg}'] def #{arg}
properties['#{arg}']
end
end end
def #{arg}=(value) def #{arg}=(value)
......
...@@ -164,12 +164,15 @@ class User < ActiveRecord::Base ...@@ -164,12 +164,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed? 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_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_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } 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 :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_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed? after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
...@@ -408,7 +411,6 @@ class User < ActiveRecord::Base ...@@ -408,7 +411,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u| 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.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.name = 'Ghost User'
u.notification_email = email
end end
end end
end end
...@@ -1044,11 +1046,6 @@ class User < ActiveRecord::Base ...@@ -1044,11 +1046,6 @@ class User < ActiveRecord::Base
end end
end end
def update_cache_counts
assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true)
end
def update_todos_count_cache def update_todos_count_cache
todos_done_count(force: true) todos_done_count(force: true)
todos_pending_count(force: true) todos_pending_count(force: true)
......
...@@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy ...@@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } 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) } condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
desc "User is the assignee or author" desc "User is the assignee or author"
......
...@@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy ...@@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue prevent :update_issue
prevent :admin_issue prevent :admin_issue
end end
rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid
end end
class MergeRequestPolicy < IssuablePolicy class MergeRequestPolicy < IssuablePolicy
rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end end
...@@ -66,6 +66,22 @@ class ProjectPolicy < BasePolicy ...@@ -66,6 +66,22 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any? project.merge_requests_allowing_push_to_user(user).any?
end 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[ features = %w[
merge_requests merge_requests
issues issues
...@@ -81,6 +97,10 @@ class ProjectPolicy < BasePolicy ...@@ -81,6 +97,10 @@ class ProjectPolicy < BasePolicy
condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) } condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) }
end 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 { guest }.enable :guest_access
rule { reporter }.enable :reporter_access rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access rule { developer }.enable :developer_access
...@@ -150,6 +170,7 @@ class ProjectPolicy < BasePolicy ...@@ -150,6 +170,7 @@ class ProjectPolicy < BasePolicy
# where we enable or prevent it based on other coditions. # where we enable or prevent it based on other coditions.
rule { (~anonymous & public_project) | internal_access }.policy do rule { (~anonymous & public_project) | internal_access }.policy do
enable :public_user_access enable :public_user_access
enable :read_project_for_iids
end end
rule { can?(:public_user_access) }.policy do rule { can?(:public_user_access) }.policy do
...@@ -255,7 +276,11 @@ class ProjectPolicy < BasePolicy ...@@ -255,7 +276,11 @@ class ProjectPolicy < BasePolicy
end end
rule { anonymous & ~public_project }.prevent_all 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 rule { can?(:public_access) }.policy do
enable :read_project enable :read_project
...@@ -305,6 +330,14 @@ class ProjectPolicy < BasePolicy ...@@ -305,6 +330,14 @@ class ProjectPolicy < BasePolicy
enable :update_pipeline enable :update_pipeline
end 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 private
def team_member? def team_member?
......
...@@ -15,6 +15,8 @@ module Ci ...@@ -15,6 +15,8 @@ module Ci
def status_title def status_title
if auto_canceled? if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
else
tooltip_for_badge
end end
end end
...@@ -28,5 +30,19 @@ module Ci ...@@ -28,5 +30,19 @@ module Ci
trigger_request.user_variables trigger_request.user_variables
end end
end end
def tooltip_message
"#{subject.name} - #{detailed_status.status_tooltip}"
end
private
def tooltip_for_badge
detailed_status.badge_tooltip.capitalize
end
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
end end
end end
class BuildMetadataEntity < Grape::Entity class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata| expose :timeout_human_readable
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_source do |metadata| expose :timeout_source do |metadata|
metadata.present.timeout_source metadata.present.timeout_source
end end
......
...@@ -2,7 +2,7 @@ class StatusEntity < Grape::Entity ...@@ -2,7 +2,7 @@ class StatusEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :icon, :text, :label, :group expose :icon, :text, :label, :group
expose :status_tooltip, as: :tooltip
expose :has_details?, as: :has_details expose :has_details?, as: :has_details
expose :details_path expose :details_path
......
...@@ -42,7 +42,10 @@ module Boards ...@@ -42,7 +42,10 @@ module Boards
) )
end 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 attrs
end end
......
...@@ -12,11 +12,15 @@ module Boards ...@@ -12,11 +12,15 @@ module Boards
private private
def available_labels_for(board) def available_labels_for(board)
options = { include_ancestor_groups: true }
if board.group_board? if board.group_board?
parent.labels options.merge!(group_id: parent.id, only_group_labels: true)
else else
LabelsFinder.new(current_user, project_id: parent.id).execute options[:project_id] = parent.id
end end
LabelsFinder.new(current_user, options).execute
end end
def next_position(board) def next_position(board)
......
...@@ -4,6 +4,9 @@ module Ci ...@@ -4,6 +4,9 @@ module Ci
class RegisterJobService class RegisterJobService
attr_reader :runner attr_reader :runner
JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?) Result = Struct.new(:build, :valid?)
def initialize(runner) def initialize(runner)
...@@ -104,10 +107,22 @@ module Ci ...@@ -104,10 +107,22 @@ module Ci
end end
def register_success(job) def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) labels = { shared_runner: runner.shared?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at)
attempt_counter.increment attempt_counter.increment
end end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
def failed_attempt_counter def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end end
...@@ -117,7 +132,7 @@ module Ci ...@@ -117,7 +132,7 @@ module Ci
end end
def job_queue_duration_seconds def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time') @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end end
end end
end end
...@@ -4,6 +4,7 @@ module Issuable ...@@ -4,6 +4,7 @@ module Issuable
TodoService.new.destroy_target(issuable) do |issuable| TodoService.new.destroy_target(issuable) do |issuable|
if issuable.destroy if issuable.destroy
issuable.update_project_counter_caches issuable.update_project_counter_caches
issuable.assignees.each(&:invalidate_cache_counts)
end end
end end
end end
......
...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService ...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService
end end
def available_labels def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
end end
def handle_quick_actions_on_create(issuable) def handle_quick_actions_on_create(issuable)
......
...@@ -55,9 +55,10 @@ module Issues ...@@ -55,9 +55,10 @@ module Issues
return unless params[:move_between_ids] return unless params[:move_between_ids]
after_id, before_id = params.delete(: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(issue.project, before_id) if before_id issue_before = get_issue_if_allowed(before_id, board_group_id)
issue_after = get_issue_if_allowed(issue.project, after_id) if after_id issue_after = get_issue_if_allowed(after_id, board_group_id)
issue.move_between(issue_before, issue_after) issue.move_between(issue_before, issue_after)
end end
...@@ -84,8 +85,16 @@ module Issues ...@@ -84,8 +85,16 @@ module Issues
private private
def get_issue_if_allowed(project, id) def get_issue_if_allowed(id, board_group_id = nil)
issue = project.issues.find(id) 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) issue if can?(current_user, :update_issue, issue)
end end
......
...@@ -11,7 +11,7 @@ module Notes ...@@ -11,7 +11,7 @@ module Notes
unless @note.system? unless @note.system?
EventCreateService.new.leave_note(@note, @note.author) EventCreateService.new.leave_note(@note, @note.author)
return unless @note.for_project_noteable? return if @note.for_personal_snippet?
@note.create_cross_references! @note.create_cross_references!
execute_note_hooks execute_note_hooks
...@@ -23,9 +23,13 @@ module Notes ...@@ -23,9 +23,13 @@ module Notes
end end
def execute_note_hooks def execute_note_hooks
return unless @note.project
note_data = hook_data note_data = hook_data
@note.project.execute_hooks(note_data, :note_hooks) hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
@note.project.execute_services(note_data, :note_hooks)
@note.project.execute_hooks(note_data, hooks_scope)
@note.project.execute_services(note_data, hooks_scope)
end end
end end
end end
...@@ -21,7 +21,8 @@ module Projects ...@@ -21,7 +21,8 @@ module Projects
end end
def labels(target = nil) def labels(target = nil)
labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title]) labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
.execute.select([:color, :title])
return labels unless target&.respond_to?(:labels) return labels unless target&.respond_to?(:labels)
......
...@@ -5,8 +5,8 @@ module Projects ...@@ -5,8 +5,8 @@ module Projects
class GitlabProjectsImportService class GitlabProjectsImportService
attr_reader :current_user, :params attr_reader :current_user, :params
def initialize(user, params) def initialize(user, import_params, override_params = nil)
@current_user, @params = user, params.dup @current_user, @params, @override_params = user, import_params.dup, override_params
end end
def execute def execute
...@@ -17,6 +17,7 @@ module Projects ...@@ -17,6 +17,7 @@ module Projects
params[:import_type] = 'gitlab_project' params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path params[:import_source] = import_upload_path
params[:import_data] = { data: { override_params: @override_params } } if @override_params
::Projects::CreateService.new(current_user, params).execute ::Projects::CreateService.new(current_user, params).execute
end end
......
...@@ -28,7 +28,7 @@ module Projects ...@@ -28,7 +28,7 @@ module Projects
end end
def save_services 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 end
def version_saver def version_saver
...@@ -55,6 +55,10 @@ module Projects ...@@ -55,6 +55,10 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end end
def lfs_saver
Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
end
def cleanup_and_notify_error def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}") Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
......
...@@ -200,7 +200,7 @@ module QuickActions ...@@ -200,7 +200,7 @@ module QuickActions
end end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any? available_labels.any?
...@@ -562,7 +562,7 @@ module QuickActions ...@@ -562,7 +562,7 @@ module QuickActions
def find_labels(labels_param) def find_labels(labels_param)
extract_references(labels_param, :label) | extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
end end
def find_label_references(labels_param) def find_label_references(labels_param)
...@@ -593,6 +593,7 @@ module QuickActions ...@@ -593,6 +593,7 @@ module QuickActions
def extract_references(arg, type) def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(arg, author: current_user) ext.analyze(arg, author: current_user)
ext.references(type) ext.references(type)
......
...@@ -429,7 +429,7 @@ module SystemNoteService ...@@ -429,7 +429,7 @@ module SystemNoteService
def cross_reference(noteable, mentioner, author) def cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner) 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) body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue) if noteable.is_a?(ExternalIssue)
...@@ -582,7 +582,7 @@ module SystemNoteService ...@@ -582,7 +582,7 @@ module SystemNoteService
text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}" text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else else
gfm_reference = mentioner.gfm_reference(noteable.project) gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
text = cross_reference_note_content(gfm_reference) text = cross_reference_note_content(gfm_reference)
notes.where(note: [text, text.capitalize]) notes.where(note: [text, text.capitalize])
end end
......
...@@ -2,6 +2,8 @@ class JobArtifactUploader < GitlabUploader ...@@ -2,6 +2,8 @@ class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
ObjectNotReadyError = Class.new(StandardError)
storage_options Gitlab.config.artifacts storage_options Gitlab.config.artifacts
def size def size
...@@ -25,6 +27,8 @@ class JobArtifactUploader < GitlabUploader ...@@ -25,6 +27,8 @@ class JobArtifactUploader < GitlabUploader
private private
def dynamic_segment def dynamic_segment
raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id
creation_date = model.created_at.utc.strftime('%Y_%m_%d') creation_date = model.created_at.utc.strftime('%Y_%m_%d')
File.join(disk_hash[0..1], disk_hash[2..3], disk_hash, File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
......
...@@ -2,6 +2,8 @@ class LegacyArtifactUploader < GitlabUploader ...@@ -2,6 +2,8 @@ class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
ObjectNotReadyError = Class.new(StandardError)
storage_options Gitlab.config.artifacts storage_options Gitlab.config.artifacts
def store_dir def store_dir
...@@ -11,6 +13,8 @@ class LegacyArtifactUploader < GitlabUploader ...@@ -11,6 +13,8 @@ class LegacyArtifactUploader < GitlabUploader
private private
def dynamic_segment def dynamic_segment
raise ObjectNotReadyError, 'Build is not ready' unless model.id
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s) File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.id.to_s)
end end
end end
...@@ -128,7 +128,7 @@ module ObjectStorage ...@@ -128,7 +128,7 @@ module ObjectStorage
end end
def direct_upload_enabled? def direct_upload_enabled?
object_store_options.direct_upload object_store_options&.direct_upload
end end
def background_upload_enabled? def background_upload_enabled?
...@@ -156,11 +156,10 @@ module ObjectStorage ...@@ -156,11 +156,10 @@ module ObjectStorage
end end
def workhorse_authorize def workhorse_authorize
if options = workhorse_remote_upload_options {
{ RemoteObject: options } RemoteObject: workhorse_remote_upload_options,
else TempPath: workhorse_local_upload_path
{ TempPath: workhorse_local_upload_path } }.compact
end
end end
def workhorse_local_upload_path def workhorse_local_upload_path
...@@ -184,6 +183,14 @@ module ObjectStorage ...@@ -184,6 +183,14 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
} }
end end
def default_object_store
if self.object_store_enabled? && self.direct_upload_enabled?
Store::REMOTE
else
Store::LOCAL
end
end
end end
# allow to configure and overwrite the filename # allow to configure and overwrite the filename
...@@ -204,12 +211,12 @@ module ObjectStorage ...@@ -204,12 +211,12 @@ module ObjectStorage
end end
def object_store 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 end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value) def object_store=(value)
@object_store = value || Store::LOCAL @object_store = value || self.class.default_object_store
@storage = storage_for(object_store) @storage = storage_for(object_store)
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
...@@ -285,16 +292,14 @@ module ObjectStorage ...@@ -285,16 +292,14 @@ module ObjectStorage
} }
end end
def store_workhorse_file!(params, identifier) def cache!(new_file = sanitized_file)
filename = params["#{identifier}.name"] # We intercept ::UploadedFile which might be stored on remote storage
# We use that for "accelerated" uploads, where we store result on remote storage
if remote_object_id = params["#{identifier}.remote_id"] if new_file.is_a?(::UploadedFile) && new_file.remote_id
store_remote_file!(remote_object_id, filename) return cache_remote_file!(new_file.remote_id, new_file.original_filename)
elsif local_path = params["#{identifier}.path"]
store_local_file!(local_path, filename)
else
raise RemoteStoreError, 'Bad file'
end end
super
end end
private private
...@@ -305,36 +310,29 @@ module ObjectStorage ...@@ -305,36 +310,29 @@ module ObjectStorage
self.file_storage? self.file_storage?
end end
def store_remote_file!(remote_object_id, filename) def cache_remote_file!(remote_object_id, original_filename)
raise RemoteStoreError, 'Missing filename' unless filename
file_path = File.join(TMP_UPLOAD_PATH, remote_object_id) file_path = File.join(TMP_UPLOAD_PATH, remote_object_id)
file_path = Pathname.new(file_path).cleanpath.to_s file_path = Pathname.new(file_path).cleanpath.to_s
raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/') raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/')
self.object_store = Store::REMOTE
# TODO: # TODO:
# This should be changed to make use of `tmp/cache` mechanism # This should be changed to make use of `tmp/cache` mechanism
# instead of using custom upload directory, # instead of using custom upload directory,
# using tmp/cache makes this implementation way easier than it is today # using tmp/cache makes this implementation way easier than it is today
CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file| CarrierWave::Storage::Fog::File.new(self, storage_for(Store::REMOTE), file_path).tap do |file|
raise RemoteStoreError, 'Missing file' unless file.exists? raise RemoteStoreError, 'Missing file' unless file.exists?
self.filename = filename # Remote stored file, we force to store on remote storage
self.file = storage.store!(file) self.object_store = Store::REMOTE
end
end
def store_local_file!(local_path, filename)
raise RemoteStoreError, 'Missing filename' unless filename
root_path = File.realpath(self.class.workhorse_local_upload_path) # TODO:
file_path = File.realpath(local_path) # We store file internally and force it to be considered as `cached`
raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path) # This makes CarrierWave to store file in permament location (copy/delete)
# once this object is saved, but not sooner
self.object_store = Store::LOCAL @cache_id = "force-to-use-cache" # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.store!(UploadedFile.new(file_path, filename)) @file = file # rubocop:disable Gitlab/ModuleWithInstanceVariables
@filename = original_filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end end
# this is a hack around CarrierWave. The #migrate method needs to be # this is a hack around CarrierWave. The #migrate method needs to be
......
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :email_author_in_body do
= f.check_box :email_author_in_body
Include author name in notification email body
.help-block
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.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :html_emails_enabled do
= f.check_box :html_emails_enabled
Enable HTML emails
.help-block
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.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
- if Gitlab.config.registry.enabled
%fieldset
%legend Container Registry
.form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
- if koding_enabled?
%fieldset
%legend Koding
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :koding_enabled do
= f.check_box :koding_enabled
Enable Koding
.help-block
Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
.form-group
= f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
.help-block
Koding has integration enabled out of the box for the
%strong gitlab
team, and you need to provide that team's URL here. Learn more in the
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
%legend PlantUML
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :plantuml_enabled do
= f.check_box :plantuml_enabled
Enable PlantUML
.form-group
= f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
.help-block
Allow rendering of
= link_to "PlantUML", "http://plantuml.com"
diagrams in Asciidoc documents using an external PlantUML service.
%fieldset
%legend#usage-statistics Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Enable version check
.help-block
GitLab will inform you if a new version is available.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
about what information is shared with GitLab Inc.
.form-group
.col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
Enable usage ping
.help-block
- if can_be_configured
To help improve GitLab and its user experience, GitLab will
periodically collect usage information.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
about what information is shared with GitLab Inc. Visit
= link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
- else
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
= succeed '.' do
= link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
%fieldset
%legend Email
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :email_author_in_body do
= f.check_box :email_author_in_body
Include author name in notification email body
.help-block
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.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :html_emails_enabled do
= f.check_box :html_emails_enabled
Enable HTML emails
.help-block
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.
%fieldset
%legend Gitaly Timeouts
.form-group
= f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_default, class: 'form-control'
.help-block
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
.form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_fast, class: 'form-control'
.help-block
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
.form-group
= f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_medium, class: 'form-control'
.help-block
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
%fieldset
%legend Web terminal
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :terminal_max_session_time, class: 'form-control'
.help-block
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
%fieldset
%legend Real-time features
.form-group
= f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
.help-block
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
%fieldset
%legend Performance optimization
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :authorized_keys_enabled do
= f.check_box :authorized_keys_enabled
Write to "authorized_keys" file
.help-block
By default, we write to the "authorized_keys" file to support Git
over SSH without additional configuration. GitLab can be optimized
to authenticate SSH keys via the database file. Only uncheck this
if you have configured your OpenSSH server to use the
AuthorizedKeysCommand. Click on the help icon for more details.
= link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
.form-actions
= f.submit 'Save', class: 'btn btn-save'
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_default, class: 'form-control'
.help-block
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
.form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_fast, class: 'form-control'
.help-block
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
.form-group
= f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_medium, class: 'form-control'
.help-block
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :koding_enabled do
= f.check_box :koding_enabled
Enable Koding
.help-block
Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
.form-group
= f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
.help-block
Koding has integration enabled out of the box for the
%strong gitlab
team, and you need to provide that team's URL here. Learn more in the
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :authorized_keys_enabled do
= f.check_box :authorized_keys_enabled
Write to "authorized_keys" file
.help-block
By default, we write to the "authorized_keys" file to support Git
over SSH without additional configuration. GitLab can be optimized
to authenticate SSH keys via the database file. Only uncheck this
if you have configured your OpenSSH server to use the
AuthorizedKeysCommand. Click on the help icon for more details.
= link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :plantuml_enabled do
= f.check_box :plantuml_enabled
Enable PlantUML
.form-group
= f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
.help-block
Allow rendering of
= link_to "PlantUML", "http://plantuml.com"
diagrams in Asciidoc documents using an external PlantUML service.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
.help-block
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :terminal_max_session_time, class: 'form-control'
.help-block
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Enable version check
.help-block
GitLab will inform you if a new version is available.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
about what information is shared with GitLab Inc.
.form-group
.col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
Enable usage ping
.help-block
- if can_be_configured
To help improve GitLab and its user experience, GitLab will
periodically collect usage information.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
about what information is shared with GitLab Inc. Visit
= link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
- else
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
= succeed '.' do
= link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
= f.submit 'Save changes', class: "btn btn-success"
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Auto DevOps, runners amd job artifacts') = _('Auto DevOps, runners and job artifacts')
.settings-content .settings-content
= render 'ci_cd' = render 'ci_cd'
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
.settings-content .settings-content
= render 'prometheus' = render 'prometheus'
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) } %section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('Profiling - Performance bar') = _('Profiling - Performance bar')
...@@ -180,6 +180,107 @@ ...@@ -180,6 +180,107 @@
.settings-content .settings-content
= render 'repository_check' = render 'repository_check'
- if Gitlab.config.registry.enabled
%section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Container Registry')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various container registry settings.')
.settings-content
= render 'registry'
- if koding_enabled?
%section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Koding')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Online IDE integration settings.')
.settings-content
= render 'koding'
%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('PlantUML')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
= render 'plantuml'
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) }
.settings-header#usage-statistics
%h4
= _('Usage statistics')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable or disable version check and usage ping.')
.settings-content
= render 'usage'
%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Email')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various email settings.')
.settings-content
= render 'email'
%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Gitaly')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure Gitaly timeouts.')
.settings-content
= render 'gitaly'
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Web terminal')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set max session time for web terminal.')
.settings-content
= render 'terminal'
%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Real-time features')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
.settings-content
= render 'realtime'
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Performance optimization')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various settings that affect GitLab performance.')
.settings-content
= render 'performance'
%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) } %section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
...@@ -201,6 +302,3 @@ ...@@ -201,6 +302,3 @@
= _('Allow requests to the local network from hooks and services.') = _('Allow requests to the local network from hooks and services.')
.settings-content .settings-content
= render 'outbound' = render 'outbound'
.prepend-top-20
= render 'form'
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.
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