summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml8
-rw-r--r--CHANGELOG.md24
-rw-r--r--app/assets/javascripts/ide/components/ide.vue5
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue83
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue57
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue61
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue109
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js6
-rw-r--r--app/assets/javascripts/ide/stores/utils.js8
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js23
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue90
-rw-r--r--app/assets/stylesheets/pages/repo.scss28
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/helpers/services_helper.rb25
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/service.rb8
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/notes/post_process_service.rb6
-rw-r--r--app/services/projects/import_export/export_service.rb6
-rw-r--r--app/uploaders/object_storage.rb14
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml3
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml7
-rw-r--r--changelogs/unreleased/42028-xss-diffs.yml5
-rw-r--r--changelogs/unreleased/bvl-export-import-lfs.yml5
-rw-r--r--changelogs/unreleased/direct-upload-of-uploads.yml5
-rw-r--r--changelogs/unreleased/dm-internal-user-namespace.yml5
-rw-r--r--changelogs/unreleased/jej-mattermost-notification-confidentiality.yml5
-rw-r--r--config/gitlab.yml.example28
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--db/migrate/20171222115326_add_confidential_note_events_to_web_hooks.rb15
-rw-r--r--db/migrate/20180103123548_add_confidential_note_events_to_services.rb16
-rw-r--r--db/post_migrate/20180104131052_schedule_set_confidential_note_events_on_webhooks.rb23
-rw-r--r--db/post_migrate/20180122154930_schedule_set_confidential_note_events_on_services.rb23
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/uploads.md1
-rw-r--r--doc/user/admin_area/settings/email.md5
-rw-r--r--doc/user/project/settings/import_export.md2
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/project_hooks.rb1
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_services.rb26
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb26
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb3
-rw-r--r--lib/gitlab/data_builder/note.rb4
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/diff/inline_diff_marker.rb7
-rw-r--r--lib/gitlab/git/hook.rb4
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb15
-rw-r--r--lib/gitlab/import_export/importer.rb11
-rw-r--r--lib/gitlab/import_export/lfs_restorer.rb43
-rw-r--r--lib/gitlab/import_export/lfs_saver.rb55
-rw-r--r--lib/gitlab/utils.rb5
-rw-r--r--spec/factories/project_hooks.rb1
-rw-r--r--spec/features/issues/form_spec.rb17
-rw-r--r--spec/fixtures/exported-project.gzbin0 -> 2306 bytes
-rw-r--r--spec/helpers/diff_helper_spec.rb30
-rw-r--r--spec/javascripts/ide/components/ide_file_buttons_spec.js (renamed from spec/javascripts/ide/components/repo_file_buttons_spec.js)26
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js60
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js11
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js41
-rw-r--r--spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb31
-rw-r--r--spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb31
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb64
-rw-r--r--spec/lib/gitlab/import_export/lfs_restorer_spec.rb75
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb62
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/mailers/previews/email_rejection_mailer_preview.rb5
-rw-r--r--spec/mailers/previews/notify_preview.rb83
-rw-r--r--spec/mailers/previews/repository_check_mailer_preview.rb5
-rw-r--r--spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb44
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb20
-rw-r--r--spec/migrations/rename_users_with_renamed_namespace_spec.rb4
-rw-r--r--spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb44
-rw-r--r--spec/models/note_spec.rb15
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb15
-rw-r--r--spec/models/project_spec.rb16
-rw-r--r--spec/models/service_spec.rb16
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb4
-rw-r--r--spec/services/notes/post_process_service_spec.rb18
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb43
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb62
-rw-r--r--spec/uploaders/object_storage_spec.rb38
94 files changed, 1696 insertions, 234 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 90aabf5034a..433487877c0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -765,7 +765,13 @@ qa:selectors:
- bundle exec bin/qa Test::Sanity::Selectors
coverage:
- <<: *dedicated-no-docs-no-db-pull-cache-job
+ # Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
+ # download artifacts from all the rspec jobs instead of from setup-test-env only
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
+ <<: *pull-cache
+ variables:
+ SETUP_DB: "false"
stage: post-test
script:
- bundle exec scripts/merge-simplecov
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6491905a1ac..9109f04fb17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.6.3 (2018-04-03)
+
+### Security (2 changes)
+
+- Fix XSS on diff view stored on filenames.
+- Adds confidential notes channel for Slack/Mattermost.
+
+
## 10.6.2 (2018-03-29)
### Fixed (2 changes, 1 of them is from the community)
@@ -217,6 +225,14 @@ entry.
- Use host URL to build JIRA remote link icon.
+## 10.5.7 (2018-04-03)
+
+### Security (2 changes)
+
+- Fix XSS on diff view stored on filenames.
+- Adds confidential notes channel for Slack/Mattermost.
+
+
## 10.5.6 (2018-03-16)
### Security (2 changes)
@@ -484,6 +500,14 @@ entry.
- Adds empty state illustration for pending job.
+## 10.4.7 (2018-04-03)
+
+### Security (2 changes)
+
+- Fix XSS on diff view stored on filenames.
+- Adds confidential notes channel for Slack/Mattermost.
+
+
## 10.4.6 (2018-03-16)
### Security (2 changes)
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index d22869466c9..1c237c0ec97 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
-import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
@@ -12,7 +11,6 @@ export default {
ideSidebar,
ideContextbar,
repoTabs,
- repoFileButtons,
ideStatusBar,
repoEditor,
},
@@ -70,9 +68,6 @@ export default {
class="multi-file-edit-pane-content"
:file="activeFile"
/>
- <repo-file-buttons
- :file="activeFile"
- />
<ide-status-bar
:file="activeFile"
/>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
new file mode 100644
index 00000000000..6d07329df71
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -0,0 +1,83 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b1a16350c19..99423362924 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -2,10 +2,16 @@
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
+import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
+import IdeFileButtons from './ide_file_buttons.vue';
export default {
+ components: {
+ ContentViewer,
+ IdeFileButtons,
+ },
props: {
file: {
type: Object,
@@ -18,6 +24,16 @@ export default {
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
+ editTabCSS() {
+ return {
+ active: this.file.viewMode === 'edit',
+ };
+ },
+ previewTabCSS() {
+ return {
+ active: this.file.viewMode === 'preview',
+ };
+ },
},
watch: {
file(oldVal, newVal) {
@@ -56,6 +72,7 @@ export default {
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
+ 'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
@@ -153,15 +170,47 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-if="shouldHideEditor"
- v-html="file.html"
- >
+ class="ide-mode-tabs clearfix"
+ v-if="!shouldHideEditor">
+ <ul class="nav-links pull-left">
+ <li :class="editTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
+ <template v-if="viewer === 'editor'">
+ {{ __('Edit') }}
+ </template>
+ <template v-else>
+ {{ __('Review') }}
+ </template>
+ </a>
+ </li>
+ <li
+ v-if="file.previewMode"
+ :class="previewTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode:'preview' })">
+ {{ file.previewMode.previewTitle }}
+ </a>
+ </li>
+ </ul>
+ <ide-file-buttons
+ :file="file"
+ />
</div>
<div
- v-show="!shouldHideEditor"
+ v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
>
</div>
+ <content-viewer
+ v-if="!shouldHideEditor && file.viewMode === 'preview'"
+ :content="file.content || file.raw"
+ :path="file.path"
+ :project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
deleted file mode 100644
index 4ea8cf7504b..00000000000
--- a/app/assets/javascripts/ide/components/repo_file_buttons.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<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>
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index faa690ecba0..5ea2a2f6825 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -1,67 +1,64 @@
<script>
- import { mapActions, mapState } from 'vuex';
- import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { mapActions, mapState } from 'vuex';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
- export default {
- components: {
- PanelResizer,
+export default {
+ components: {
+ PanelResizer,
+ },
+ props: {
+ collapsible: {
+ type: Boolean,
+ required: true,
},
- props: {
- collapsible: {
- type: Boolean,
- required: true,
- },
- initialWidth: {
- type: Number,
- required: true,
- },
- minSize: {
- type: Number,
- required: false,
- default: 200,
- },
- side: {
- type: String,
- required: true,
- },
+ initialWidth: {
+ type: Number,
+ required: true,
},
- data() {
- return {
- width: this.initialWidth,
- };
+ minSize: {
+ type: Number,
+ required: false,
+ default: 340,
},
- computed: {
- ...mapState({
- collapsed(state) {
- return state[`${this.side}PanelCollapsed`];
- },
- }),
- panelStyle() {
- if (!this.collapsed) {
- return {
- width: `${this.width}px`,
- };
- }
-
- return {};
- },
+ side: {
+ type: String,
+ required: true,
},
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleFullbarCollapsed() {
- if (this.collapsed && this.collapsible) {
- this.setPanelCollapsedStatus({
- side: this.side,
- collapsed: !this.collapsed,
- });
- }
+ },
+ data() {
+ return {
+ width: this.initialWidth,
+ };
+ },
+ computed: {
+ ...mapState({
+ collapsed(state) {
+ return state[`${this.side}PanelCollapsed`];
},
+ }),
+ panelStyle() {
+ if (!this.collapsed) {
+ return {
+ width: `${this.width}px`,
+ };
+ }
+
+ return {};
+ },
+ },
+ methods: {
+ ...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
+ toggleFullbarCollapsed() {
+ if (this.collapsed && this.collapsible) {
+ this.setPanelCollapsedStatus({
+ side: this.side,
+ collapsed: !this.collapsed,
+ });
+ }
},
- maxSize: (window.innerWidth / 2),
- };
+ },
+ maxSize: window.innerWidth / 2,
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 6b034ea1e82..1a17320a1ea 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
+export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
+ commit(types.SET_FILE_VIEWMODE, { file, viewMode });
+};
+
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index ee759bff516..e3f504e5ab0 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 926b6f66d78..6a143e518f9 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -42,6 +42,7 @@ export default {
renderError: data.render_error,
raw: null,
baseRaw: null,
+ html: data.html,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -83,6 +84,11 @@ export default {
mrChange,
});
},
+ [types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
+ Object.assign(state.entries[file.path], {
+ viewMode,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 63e4de3b17d..4befcc501ef 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,6 +38,8 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
+ viewMode: 'edit',
+ previewMode: null,
});
export const decorateData = entity => {
@@ -57,8 +59,9 @@ export const decorateData = entity => {
changed = false,
parentTreeUrl = '',
base64 = false,
-
+ previewMode,
file_lock,
+ html,
} = entity;
return {
@@ -79,8 +82,9 @@ export const decorateData = entity => {
renderError,
content,
base64,
-
+ previewMode,
file_lock,
+ html,
};
};
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a4cd1ab099f..a1673276900 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,14 +1,8 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => {
- const {
- data,
- projectId,
- branchId,
- tempFile = false,
- content = '',
- base64 = false,
- } = e.data;
+ const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
- const folderPath = `${
- parentFolder ? `${parentFolder.path}/` : ''
- }${folderName}`;
+ const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree',
- parentTreeUrl: parentFolder
- ? parentFolder.url
- : `/${projectId}/tree/${branchId}/`,
+ parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
- parentTreeUrl: fileFolder
- ? fileFolder.url
- : `/${projectId}/blob/${branchId}`,
+ parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
+ previewMode: viewerInformationForPath(blobName),
});
Object.assign(acc, {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index add07c156a4..c749042a14a 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -94,10 +94,10 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
- <li data-milestone-id="${milestone.name}">
+ <li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
@@ -125,7 +125,6 @@ export default class MilestoneSelect {
return milestone.id;
}
},
- isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
@@ -137,7 +136,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
@@ -158,6 +157,7 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
+
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
new file mode 100644
index 00000000000..fb8ccea91c7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -0,0 +1,43 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
new file mode 100644
index 00000000000..4f2e1e47dd1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -0,0 +1,23 @@
+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;
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
new file mode 100644
index 00000000000..09e0094054d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -0,0 +1,90 @@
+<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>
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 1f6f7138e1f..8cc5c8fc877 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -308,14 +308,34 @@
height: 100%;
}
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
+
+.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
- background: $white-light;
+
+ .nav-links {
+ border-bottom: 0;
+
+ li a {
+ padding: $gl-padding-8 $gl-padding;
+ line-height: $gl-btn-line-height;
+ }
+ }
+}
+
+.ide-btn-group {
+ padding: $gl-padding-4 $gl-vert-padding;
}
.ide-status-bar {
+ border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index c77f10ef1dd..ee4ed674110 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids
@existing_oids ||= begin
- storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+ project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index f435c80c656..f872990122e 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,4 +1,29 @@
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)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b6dd39b860b..ec072882cc9 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -7,6 +7,7 @@ class ProjectHook < WebHook
:issue_hooks,
:confidential_issue_hooks,
:note_hooks,
+ :confidential_note_hooks,
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
diff --git a/app/models/note.rb b/app/models/note.rb
index 0f5fb529a87..109405d3f17 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -268,6 +268,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
+ def confidential?
+ noteable.try(:confidential?)
+ end
+
def editable?
!system?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 714a15ade9c..32289106f28 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1066,6 +1066,16 @@ class Project < ActiveRecord::Base
end
end
+ # This will return all `lfs_objects` that are accessible to the project.
+ # So this might be `self.lfs_objects` if the project is not part of a fork
+ # network, or it is the base of the fork network.
+ #
+ # TODO: refactor this to get the correct lfs objects when implementing
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ def all_lfs_objects
+ lfs_storage_project.lfs_objects
+ end
+
def personal?
!group
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index dab0ea1a681..7591ab4f478 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end
end
+ def confidential_issue_channel
+ properties['confidential_issue_channel'].presence || properties['issue_channel']
+ end
+
+ def confidential_note_channel
+ properties['confidential_note_channel'].presence || properties['note_channel']
+ end
+
def self.supported_events
- %w[push issue confidential_issue merge_request note tag_push
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message
- channel_name = get_channel_field(object_kind).presence || channel
+ event_type = data[:event_type] || object_kind
+
+ channel_name = get_channel_field(event_type).presence || channel
opts = {}
opts[:channel] = channel_name if channel_name
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index f31c3f02af2..dce878e485f 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -46,7 +46,7 @@ class HipchatService < Service
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push pipeline)
+ %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
end
def execute(data)
diff --git a/app/models/service.rb b/app/models/service.rb
index 7424cef0fc0..e9b6f005aec 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
+ default_value_for :confidential_note_events, true
default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
@@ -42,6 +43,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
+ scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
@@ -168,8 +170,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
- def #{arg}
- properties['#{arg}']
+ unless method_defined?(arg)
+ def #{arg}
+ properties['#{arg}']
+ end
end
def #{arg}=(value)
diff --git a/app/models/user.rb b/app/models/user.rb
index ba51595e6a3..7b6857a0d34 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -164,12 +164,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
+ before_save :set_notification_email, if: :email_changed? # in case validation is skipped
before_validation :set_public_email, if: :public_email_changed?
+ before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
+ before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
@@ -408,7 +411,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.name = 'Ghost User'
- u.notification_email = email
end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index ad3dcc5010b..f0cab2ade6d 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -24,8 +24,10 @@ module Notes
def execute_note_hooks
note_data = hook_data
- @note.project.execute_hooks(note_data, :note_hooks)
- @note.project.execute_services(note_data, :note_hooks)
+ hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
+
+ @note.project.execute_hooks(note_data, hooks_scope)
+ @note.project.execute_services(note_data, hooks_scope)
end
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 402cddd3ec1..7bf0b90b491 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -28,7 +28,7 @@ module Projects
end
def save_services
- [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
end
def version_saver
@@ -55,6 +55,10 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
+ def lfs_saver
+ Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
+ end
+
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 4028b052768..3f59ee39299 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -128,7 +128,7 @@ module ObjectStorage
end
def direct_upload_enabled?
- object_store_options.direct_upload
+ object_store_options&.direct_upload
end
def background_upload_enabled?
@@ -184,6 +184,14 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
+
+ def default_object_store
+ if self.object_store_enabled? && self.direct_upload_enabled?
+ Store::REMOTE
+ else
+ Store::LOCAL
+ end
+ end
end
# allow to configure and overwrite the filename
@@ -204,12 +212,12 @@ module ObjectStorage
end
def object_store
- @object_store ||= model.try(store_serialization_column) || Store::LOCAL
+ @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
- @object_store = value || Store::LOCAL
+ @object_store = value || self.class.default_object_store
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index 6693e6f90e8..af518b5b583 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,4 +1,3 @@
Unfortunately, your email message to GitLab could not be processed.
-
-
+\
= @reason
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 825bfd0707f..1e7d9444986 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -21,11 +21,11 @@
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %li LFS objects
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
- %li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ad4d39b4aa1..d36ca032558 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -33,6 +33,13 @@
%p.light
This URL will be triggered when someone adds a comment
%li
+ = form.check_box :confidential_note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :confidential_note_events, class: 'list-label' do
+ %strong Confidential Comments
+ %p.light
+ This URL will be triggered when someone adds a comment on a confidential issue
+ %li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= form.label :issues_events, class: 'list-label' do
diff --git a/changelogs/unreleased/42028-xss-diffs.yml b/changelogs/unreleased/42028-xss-diffs.yml
new file mode 100644
index 00000000000..a05f9d3c78d
--- /dev/null
+++ b/changelogs/unreleased/42028-xss-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix XSS on diff view stored on filenames
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/bvl-export-import-lfs.yml b/changelogs/unreleased/bvl-export-import-lfs.yml
new file mode 100644
index 00000000000..dd1f499c3a3
--- /dev/null
+++ b/changelogs/unreleased/bvl-export-import-lfs.yml
@@ -0,0 +1,5 @@
+---
+title: Support LFS objects when importing/exporting GitLab project archives
+merge_request: 18115
+author:
+type: added
diff --git a/changelogs/unreleased/direct-upload-of-uploads.yml b/changelogs/unreleased/direct-upload-of-uploads.yml
new file mode 100644
index 00000000000..7900fa5f58d
--- /dev/null
+++ b/changelogs/unreleased/direct-upload-of-uploads.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to store uploads by default on Object Storage
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/dm-internal-user-namespace.yml b/changelogs/unreleased/dm-internal-user-namespace.yml
new file mode 100644
index 00000000000..8517c116795
--- /dev/null
+++ b/changelogs/unreleased/dm-internal-user-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure internal users (ghost, support bot) get assigned a namespace
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml b/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
new file mode 100644
index 00000000000..d5219b5d019
--- /dev/null
+++ b/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
@@ -0,0 +1,5 @@
+---
+title: Adds confidential notes channel for Slack/Mattermost
+merge_request:
+author:
+type: security
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 126a9b8b803..8c39a1f2aa9 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -154,7 +154,7 @@ production: &base
# provider: AWS # Only AWS supported at the moment
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- # region: eu-central-1
+ # region: us-east-1
## Git LFS
lfs:
@@ -164,13 +164,14 @@ production: &base
object_store:
enabled: false
remote_directory: lfs-objects # Bucket name
+ # direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: eu-central-1
+ region: us-east-1
# Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
@@ -183,17 +184,18 @@ production: &base
# base_dir: uploads/-/system
object_store:
enabled: false
- # remote_directory: uploads # Bucket name
- # background_upload: false # Temporary option to limit automatic upload (Default: true)
- # proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
- # connection:
- # provider: AWS
- # aws_access_key_id: AWS_ACCESS_KEY_ID
- # aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- # region: eu-central-1
- # host: 'localhost' # default: s3.amazonaws.com
- # endpoint: 'http://127.0.0.1:9000' # default: nil
- # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
+ # remote_directory: uploads # Bucket name
+ # direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
+ # background_upload: false # Temporary option to limit automatic upload (Default: true)
+ # proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
+ connection:
+ provider: AWS
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
+ # host: 'localhost' # default: s3.amazonaws.com
+ # endpoint: 'http://127.0.0.1:9000' # default: nil
+ # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 187e70868ea..7bb863e2278 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -365,6 +365,7 @@ Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system
Settings.uploads['object_store'] ||= Settingslogic.new({})
Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
+Settings.uploads['object_store']['direct_upload'] = false if Settings.uploads['object_store']['direct_upload'].nil?
Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
Settings.uploads['object_store']['proxy_download'] = false if Settings.uploads['object_store']['proxy_download'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
diff --git a/db/migrate/20171222115326_add_confidential_note_events_to_web_hooks.rb b/db/migrate/20171222115326_add_confidential_note_events_to_web_hooks.rb
new file mode 100644
index 00000000000..900a6386922
--- /dev/null
+++ b/db/migrate/20171222115326_add_confidential_note_events_to_web_hooks.rb
@@ -0,0 +1,15 @@
+class AddConfidentialNoteEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :web_hooks, :confidential_note_events, :boolean
+ end
+
+ def down
+ remove_column :web_hooks, :confidential_note_events
+ end
+end
diff --git a/db/migrate/20180103123548_add_confidential_note_events_to_services.rb b/db/migrate/20180103123548_add_confidential_note_events_to_services.rb
new file mode 100644
index 00000000000..b54ad88df43
--- /dev/null
+++ b/db/migrate/20180103123548_add_confidential_note_events_to_services.rb
@@ -0,0 +1,16 @@
+class AddConfidentialNoteEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :services, :confidential_note_events, :boolean
+ change_column_default :services, :confidential_note_events, true
+ end
+
+ def down
+ remove_column :services, :confidential_note_events
+ end
+end
diff --git a/db/post_migrate/20180104131052_schedule_set_confidential_note_events_on_webhooks.rb b/db/post_migrate/20180104131052_schedule_set_confidential_note_events_on_webhooks.rb
new file mode 100644
index 00000000000..fa51ac83619
--- /dev/null
+++ b/db/post_migrate/20180104131052_schedule_set_confidential_note_events_on_webhooks.rb
@@ -0,0 +1,23 @@
+class ScheduleSetConfidentialNoteEventsOnWebhooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ INTERVAL = 5.minutes
+
+ disable_ddl_transaction!
+
+ def up
+ migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks
+ migration_name = migration.to_s.demodulize
+ relation = migration::WebHook.hooks_to_update
+
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ migration_name,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180122154930_schedule_set_confidential_note_events_on_services.rb b/db/post_migrate/20180122154930_schedule_set_confidential_note_events_on_services.rb
new file mode 100644
index 00000000000..a3ff9f1719e
--- /dev/null
+++ b/db/post_migrate/20180122154930_schedule_set_confidential_note_events_on_services.rb
@@ -0,0 +1,23 @@
+class ScheduleSetConfidentialNoteEventsOnServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ INTERVAL = 20.minutes
+
+ disable_ddl_transaction!
+
+ def up
+ migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices
+ migration_name = migration.to_s.demodulize
+ relation = migration::Service.services_to_update
+
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ migration_name,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1dd87c12144..2cd51b200b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1684,6 +1684,7 @@ ActiveRecord::Schema.define(version: 20180405101928) do
t.boolean "confidential_issues_events", default: true, null: false
t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false
+ t.boolean "confidential_note_events", default: true
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -2022,6 +2023,7 @@ ActiveRecord::Schema.define(version: 20180405101928) do
t.boolean "confidential_issues_events", default: false, null: false
t.boolean "repository_update_events", default: false, null: false
t.boolean "job_events", default: false, null: false
+ t.boolean "confidential_note_events"
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 60a45426636..c8f27719ce9 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -85,7 +85,6 @@ created in snippets, wikis, and repos.
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
-server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
[reply by email]: reply_by_email.md
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index a82735cc72c..2fa3284b6be 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -65,6 +65,7 @@ For source installations the following settings are nested under `uploads:` and
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Uploads will be stored| |
+| `direct_upload` | Set to true to enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. This is beta option as it uses inefficient way of uploading data (via Unicorn). The accelerated uploads gonna be implemented in future releases | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
new file mode 100644
index 00000000000..7c9e5bf882e
--- /dev/null
+++ b/doc/user/admin_area/settings/email.md
@@ -0,0 +1,5 @@
+# Email
+
+## Custom logo
+
+The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dedf102fc37..eb0ac221e30 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -57,11 +57,11 @@ The following items will be exported:
- Project configuration including web hooks and services
- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
and other project entities
+- LFS objects
The following items will NOT be exported:
- Build traces and artifacts
-- LFS objects
- Container registry images
- CI variables
- Any encrypted tokens
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e5ecd37e473..e35b1a0ff63 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -72,7 +72,7 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :confidential_issues_events
- expose :note_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events
expose :job_events
end
@@ -794,7 +794,7 @@ module API
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :confidential_issues_events
expose :merge_requests_events, :tag_push_events, :note_events
- expose :pipeline_events, :wiki_page_events
+ expose :confidential_note_events, :pipeline_events, :wiki_page_events
expose :job_events
# Expose serialized properties
expose :properties do |service, options|
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index f82241058e5..68921ae439b 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -14,6 +14,7 @@ module API
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
+ optional :confidential_note_events, type: Boolean, desc: "Trigger hook on confidential note(comment) events"
optional :job_events, type: Boolean, desc: "Trigger hook on job events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
new file mode 100644
index 00000000000..e5e8837221e
--- /dev/null
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ # Ensures services which previously recieved all notes events continue
+ # to recieve confidential ones.
+ class SetConfidentialNoteEventsOnServices
+ class Service < ActiveRecord::Base
+ self.table_name = 'services'
+
+ include ::EachBatch
+
+ def self.services_to_update
+ where(confidential_note_events: nil, note_events: true)
+ end
+ end
+
+ def perform(start_id, stop_id)
+ Service.services_to_update
+ .where(id: start_id..stop_id)
+ .update_all(confidential_note_events: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
new file mode 100644
index 00000000000..171c8ef21b7
--- /dev/null
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ # Ensures hooks which previously recieved all notes events continue
+ # to recieve confidential ones.
+ class SetConfidentialNoteEventsOnWebhooks
+ class WebHook < ActiveRecord::Base
+ self.table_name = 'web_hooks'
+
+ include ::EachBatch
+
+ def self.hooks_to_update
+ where(confidential_note_events: nil, note_events: true)
+ end
+ end
+
+ def perform(start_id, stop_id)
+ WebHook.hooks_to_update
+ .where(id: start_id..stop_id)
+ .update_all(confidential_note_events: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index f7276a380dc..f0e5773ec3c 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -15,8 +15,7 @@ module Gitlab
return false unless new_lfs_pointers.present?
- existing_count = @project.lfs_storage_project
- .lfs_objects
+ existing_count = @project.all_lfs_objects
.where(oid: new_lfs_pointers.map(&:lfs_oid))
.count
diff --git a/lib/gitlab/data_builder/note.rb b/lib/gitlab/data_builder/note.rb
index 50fea1232af..f573368e572 100644
--- a/lib/gitlab/data_builder/note.rb
+++ b/lib/gitlab/data_builder/note.rb
@@ -9,6 +9,7 @@ module Gitlab
#
# data = {
# object_kind: "note",
+ # event_type: "confidential_note",
# user: {
# name: String,
# username: String,
@@ -51,8 +52,11 @@ module Gitlab
end
def build_base_data(project, user, note)
+ event_type = note.confidential? ? 'confidential_note' : 'note'
+
base_data = {
object_kind: "note",
+ event_type: event_type,
user: user.hook_attrs,
project_id: project.id,
project: project.hook_attrs,
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 1634fe4e9cb..77079e5e72b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -860,7 +860,7 @@ into similar problems in the future (e.g. when new tables are created).
# Each job is scheduled with a `delay_interval` in between.
# If you use a small interval, then some jobs may run at the same time.
#
- # model_class - The table being iterated over
+ # model_class - The table or relation being iterated over
# job_class_name - The background migration job class as a string
# delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
# batch_size - The maximum number of rows per job
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index 010b4be7b40..81e91ea0ab7 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -1,11 +1,14 @@
module Gitlab
module Diff
class InlineDiffMarker < Gitlab::StringRangeMarker
+ def initialize(line, rich_line = nil)
+ super(line, rich_line || line)
+ end
+
def mark(line_inline_diffs, mode: nil)
- mark = super(line_inline_diffs) do |text, left:, right:|
+ super(line_inline_diffs) do |text, left:, right:|
%{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}
end
- mark.html_safe
end
private
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 24f027d8da4..7c201c6169b 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -95,13 +95,13 @@ module Gitlab
args = [ref, oldrev, newrev]
stdout, stderr, status = Open3.capture3(env, path, *args, options)
- [status.success?, (stderr.presence || stdout).gsub(/\R/, "<br>").html_safe]
+ [status.success?, Gitlab::Utils.nlbr(stderr.presence || stdout)]
end
def retrieve_error_message(stderr, stdout)
err_message = stderr.read
err_message = err_message.blank? ? stdout.read : err_message
- err_message.gsub(/\R/, "<br>").html_safe
+ Gitlab::Utils.nlbr(err_message)
end
end
end
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
index 4febb0ab430..6ab36676127 100644
--- a/lib/gitlab/hook_data/issuable_builder.rb
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -11,7 +11,8 @@ module Gitlab
def build(user: nil, changes: {})
hook_data = {
- object_kind: issuable.class.name.underscore,
+ object_kind: object_kind,
+ event_type: event_type,
user: user.hook_attrs,
project: issuable.project.hook_attrs,
object_attributes: issuable.hook_attrs,
@@ -36,6 +37,18 @@ module Gitlab
private
+ def object_kind
+ issuable.class.name.underscore
+ end
+
+ def event_type
+ if issuable.try(:confidential?)
+ "confidential_#{object_kind}"
+ else
+ object_kind
+ end
+ end
+
def issuable_builder
case issuable
when Issue
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index c38df9102eb..c490bf059d2 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def execute
- if import_file && check_version! && [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer].all?(&:restore)
+ if import_file && check_version! && restorers.all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
@@ -24,6 +24,11 @@ module Gitlab
private
+ def restorers
+ [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
+ uploads_restorer, lfs_restorer]
+ end
+
def import_file
Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
shared: @shared)
@@ -60,6 +65,10 @@ module Gitlab
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
+ def lfs_restorer
+ Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
+ end
+
def path_with_namespace
File.join(@project.namespace.full_path, @project.path)
end
diff --git a/lib/gitlab/import_export/lfs_restorer.rb b/lib/gitlab/import_export/lfs_restorer.rb
new file mode 100644
index 00000000000..b28c3c161b7
--- /dev/null
+++ b/lib/gitlab/import_export/lfs_restorer.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ImportExport
+ class LfsRestorer
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def restore
+ return true if lfs_file_paths.empty?
+
+ lfs_file_paths.each do |file_path|
+ link_or_create_lfs_object!(file_path)
+ end
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def link_or_create_lfs_object!(path)
+ size = File.size(path)
+ oid = LfsObject.calculate_oid(path)
+
+ lfs_object = LfsObject.find_or_initialize_by(oid: oid, size: size)
+ lfs_object.file = File.open(path) unless lfs_object.file&.exists?
+
+ @project.all_lfs_objects << lfs_object
+ end
+
+ def lfs_file_paths
+ @lfs_file_paths ||= Dir.glob("#{lfs_storage_path}/*")
+ end
+
+ def lfs_storage_path
+ File.join(@shared.export_path, 'lfs-objects')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
new file mode 100644
index 00000000000..29410e2331c
--- /dev/null
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module ImportExport
+ class LfsSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ @project.all_lfs_objects.each do |lfs_object|
+ save_lfs_object(lfs_object)
+ end
+
+ true
+ rescue => e
+ @shared.error(e)
+
+ false
+ end
+
+ private
+
+ def save_lfs_object(lfs_object)
+ if lfs_object.local_store?
+ copy_file_for_lfs_object(lfs_object)
+ else
+ download_file_for_lfs_object(lfs_object)
+ end
+ end
+
+ def download_file_for_lfs_object(lfs_object)
+ destination = destination_path_for_object(lfs_object)
+ mkdir_p(File.dirname(destination))
+
+ File.open(destination, 'w') do |file|
+ IO.copy_stream(URI.parse(lfs_object.file.url).open, file)
+ end
+ end
+
+ def copy_file_for_lfs_object(lfs_object)
+ copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
+ end
+
+ def destination_path_for_object(lfs_object)
+ File.join(lfs_export_path, lfs_object.oid)
+ end
+
+ def lfs_export_path
+ File.join(@shared.export_path, 'lfs-objects')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index dc9391f32cf..b0a492eaa58 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -27,6 +27,11 @@ module Gitlab
.gsub(/(\A-+|-+\z)/, '')
end
+ # Converts newlines into HTML line break elements
+ def nlbr(str)
+ ActionView::Base.full_sanitizer.sanitize(str, tags: []).gsub(/\r?\n/, '<br>').html_safe
+ end
+
def remove_line_breaks(str)
str.gsub(/\r?\n/, '')
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 493b7bc021c..a448d565e4b 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -15,6 +15,7 @@ FactoryBot.define do
issues_events true
confidential_issues_events true
note_events true
+ confidential_note_events true
job_events true
pipeline_events true
wiki_page_events true
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 38c618d300e..4625a50b8d9 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -226,6 +226,23 @@ describe 'New/edit issue', :js do
expect(page).to have_selector('.atwho-view')
end
+
+ describe 'milestone' do
+ let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
+
+ it 'escapes milestone' do
+ click_button 'Milestone'
+
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ expect(page).not_to have_selector 'img'
+ end
+ end
+ end
end
context 'edit issue' do
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
new file mode 100644
index 00000000000..352384f16c8
--- /dev/null
+++ b/spec/fixtures/exported-project.gz
Binary files differ
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 15cbe36ae76..53c010fa0db 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -135,11 +135,37 @@ describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
- expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>})
+ expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
+ expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
expect(marked_new_line).to be_html_safe
end
+
+ context 'when given HTML' do
+ it 'sanitizes it' do
+ old_line = %{test.txt}
+ new_line = %{<img src=x onerror=alert(document.domain)>}
+
+ marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
+
+ expect(marked_old_line).to eq(%q{<span class="idiff left right deletion">test.txt</span>})
+ expect(marked_old_line).to be_html_safe
+ expect(marked_new_line).to eq(%q{<span class="idiff left right addition">&lt;img src=x onerror=alert(document.domain)&gt;</span>})
+ expect(marked_new_line).to be_html_safe
+ end
+
+ it 'sanitizes the entire line, not just the changes' do
+ old_line = %{<img src=x onerror=alert(document.domain)>}
+ new_line = %{<img src=y onerror=alert(document.domain)>}
+
+ marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
+
+ expect(marked_old_line).to eq(%q{&lt;img src=<span class="idiff left right deletion">x</span> onerror=alert(document.domain)&gt;})
+ expect(marked_old_line).to be_html_safe
+ expect(marked_new_line).to eq(%q{&lt;img src=<span class="idiff left right addition">y</span> onerror=alert(document.domain)&gt;})
+ expect(marked_new_line).to be_html_safe
+ end
+ end
end
describe '#parallel_diff_discussions' do
diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js
index c86bdb132b4..8ac8d1b2acf 100644
--- a/spec/javascripts/ide/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/ide/components/ide_file_buttons_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
+import repoFileButtons from '~/ide/components/ide_file_buttons.vue';
import createVueComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
@@ -23,7 +23,7 @@ describe('RepoFileButtons', () => {
vm.$destroy();
});
- it('renders Raw, Blame, History, Permalink and Preview toggle', done => {
+ it('renders Raw, Blame, History and Permalink', done => {
vm = createComponent();
vm.$nextTick(() => {
@@ -32,16 +32,30 @@ describe('RepoFileButtons', () => {
const history = vm.$el.querySelector('.history');
expect(raw.href).toMatch(`/${activeFile.rawPath}`);
- expect(raw.textContent.trim()).toEqual('Raw');
+ expect(raw.getAttribute('data-original-title')).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blamePath}`);
- expect(blame.textContent.trim()).toEqual('Blame');
+ expect(blame.getAttribute('data-original-title')).toEqual('Blame');
expect(history.href).toMatch(`/${activeFile.commitsPath}`);
- expect(history.textContent.trim()).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual(
+ expect(history.getAttribute('data-original-title')).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual(
'Permalink',
);
done();
});
});
+
+ it('renders Download', done => {
+ activeFile.binary = true;
+ vm = createComponent();
+
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.getAttribute('data-original-title')).toEqual('Download');
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 9d3fa1280f4..e79b85050b2 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -19,7 +19,6 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
- f.html = 'testing';
vm.$store.state.openFiles.push(f);
vm.$store.state.entries[f.path] = f;
vm.monaco = true;
@@ -47,6 +46,61 @@ describe('RepoEditor', () => {
});
});
+ it('renders only an edit tab', done => {
+ Vue.nextTick(() => {
+ const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
+ expect(tabs.length).toBe(1);
+ expect(tabs[0].textContent.trim()).toBe('Edit');
+
+ done();
+ });
+ });
+
+ describe('when file is markdown', () => {
+ beforeEach(done => {
+ vm.file.previewMode = {
+ id: 'markdown',
+ previewTitle: 'Preview Markdown',
+ };
+
+ vm.$nextTick(done);
+ });
+
+ it('renders an Edit and a Preview Tab', done => {
+ Vue.nextTick(() => {
+ const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
+ expect(tabs.length).toBe(2);
+ expect(tabs[0].textContent.trim()).toBe('Edit');
+ expect(tabs[1].textContent.trim()).toBe('Preview Markdown');
+
+ done();
+ });
+ });
+ });
+
+ describe('when file is markdown and viewer mode is review', () => {
+ beforeEach(done => {
+ vm.file.previewMode = {
+ id: 'markdown',
+ previewTitle: 'Preview Markdown',
+ };
+ vm.$store.state.viewer = 'diff';
+
+ vm.$nextTick(done);
+ });
+
+ it('renders an Edit and a Preview Tab', done => {
+ Vue.nextTick(() => {
+ const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
+ expect(tabs.length).toBe(2);
+ expect(tabs[0].textContent.trim()).toBe('Review');
+ expect(tabs[1].textContent.trim()).toBe('Preview Markdown');
+
+ done();
+ });
+ });
+ });
+
describe('when open file is binary and not raw', () => {
beforeEach(done => {
vm.file.binary = true;
@@ -57,10 +111,6 @@ describe('RepoEditor', () => {
it('does not render the IDE', () => {
expect(vm.shouldHideEditor).toBeTruthy();
});
-
- it('shows activeFile html', () => {
- expect(vm.$el.textContent).toContain('testing');
- });
});
describe('createEditorInstance', () => {
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 88285ee409f..bf9d5166d0a 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -194,6 +194,17 @@ describe('IDE store file mutations', () => {
});
});
+ describe('SET_FILE_VIEWMODE', () => {
+ it('updates file view mode', () => {
+ mutations.SET_FILE_VIEWMODE(localState, {
+ file: localFile,
+ viewMode: 'preview',
+ });
+
+ expect(localFile.viewMode).toBe('preview');
+ });
+ });
+
describe('ADD_PENDING_TAB', () => {
beforeEach(() => {
const f = {
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
new file mode 100644
index 00000000000..c7c454a0b45
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('ContentViewer', () => {
+ let vm;
+ let mock;
+
+ function createComponent(props) {
+ const ContentViewer = Vue.extend(contentViewer);
+ vm = mountComponent(ContentViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ if (mock) mock.restore();
+ });
+
+ it('markdown preview renders + loads rendered markdown from server', done => {
+ mock = new MockAdapter(axios);
+ mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, {
+ body: '<b>testing</b>',
+ });
+
+ createComponent({
+ path: 'test.md',
+ content: '* Test',
+ projectPath: 'testproject',
+ });
+
+ const previewContainer = vm.$el.querySelector('.md-previewer');
+
+ setTimeout(() => {
+ expect(previewContainer.textContent).toContain('testing');
+
+ done();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb
new file mode 100644
index 00000000000..6f3fb994f17
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices, :migration, schema: 20180122154930 do
+ let(:services) { table(:services) }
+
+ describe '#perform' do
+ it 'migrates services where note_events is true' do
+ service = services.create(confidential_note_events: nil, note_events: true)
+
+ subject.perform(service.id, service.id)
+
+ expect(service.reload.confidential_note_events).to eq(true)
+ end
+
+ it 'ignores services where note_events is false' do
+ service = services.create(confidential_note_events: nil, note_events: false)
+
+ subject.perform(service.id, service.id)
+
+ expect(service.reload.confidential_note_events).to eq(nil)
+ end
+
+ it 'ignores services where confidential_note_events has already been set' do
+ service = services.create(confidential_note_events: false, note_events: true)
+
+ subject.perform(service.id, service.id)
+
+ expect(service.reload.confidential_note_events).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb
new file mode 100644
index 00000000000..82b484b7d5b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks, :migration, schema: 20180104131052 do
+ let(:web_hooks) { table(:web_hooks) }
+
+ describe '#perform' do
+ it 'migrates hooks where note_events is true' do
+ hook = web_hooks.create(confidential_note_events: nil, note_events: true)
+
+ subject.perform(hook.id, hook.id)
+
+ expect(hook.reload.confidential_note_events).to eq(true)
+ end
+
+ it 'ignores hooks where note_events is false' do
+ hook = web_hooks.create(confidential_note_events: nil, note_events: false)
+
+ subject.perform(hook.id, hook.id)
+
+ expect(hook.reload.confidential_note_events).to eq(nil)
+ end
+
+ it 'ignores hooks where confidential_note_events has already been set' do
+ hook = web_hooks.create(confidential_note_events: false, note_events: true)
+
+ subject.perform(hook.id, hook.id)
+
+ expect(hook.reload.confidential_note_events).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index aaa42566a4d..4f8412108ba 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -55,6 +55,14 @@ describe Gitlab::DataBuilder::Note do
.to be > issue.hook_attrs['updated_at']
end
+ context 'with confidential issue' do
+ let(:issue) { create(:issue, project: project, confidential: true) }
+
+ it 'sets event_type to confidential_note' do
+ expect(data[:event_type]).to eq('confidential_note')
+ end
+ end
+
include_examples 'project hook data'
include_examples 'deprecated repository hook data'
end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
new file mode 100644
index 00000000000..d75416f2a62
--- /dev/null
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Importer do
+ let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
+ let(:shared) { project.import_export_shared }
+ let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
+
+ subject(:importer) { described_class.new(project) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
+ FileUtils.mkdir_p(shared.export_path)
+ FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
+ end
+
+ after do
+ FileUtils.rm_rf(test_path)
+ end
+
+ describe '#execute' do
+ it 'succeeds' do
+ importer.execute
+
+ expect(shared.errors).to be_empty
+ end
+
+ it 'extracts the archive' do
+ expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original
+
+ importer.execute
+ end
+
+ it 'checks the version' do
+ expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original
+
+ importer.execute
+ end
+
+ context 'all restores are executed' do
+ [
+ Gitlab::ImportExport::AvatarRestorer,
+ Gitlab::ImportExport::RepoRestorer,
+ Gitlab::ImportExport::WikiRestorer,
+ Gitlab::ImportExport::UploadsRestorer,
+ Gitlab::ImportExport::LfsRestorer
+ ].each do |restorer|
+ it "calls the #{restorer}" do
+ fake_restorer = double(restorer.to_s)
+
+ expect(fake_restorer).to receive(:restore).and_return(true).at_least(1)
+ expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1)
+
+ importer.execute
+ end
+ end
+
+ it 'restores the ProjectTree' do
+ expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original
+
+ importer.execute
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
new file mode 100644
index 00000000000..70eeb9ee66b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::LfsRestorer do
+ include UploadHelpers
+
+ let(:export_path) { "#{Dir.tmpdir}/lfs_object_restorer_spec" }
+ let(:project) { create(:project) }
+ let(:shared) { project.import_export_shared }
+ subject(:restorer) { described_class.new(project: project, shared: shared) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ FileUtils.mkdir_p(shared.export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(shared.export_path)
+ end
+
+ describe '#restore' do
+ context 'when the archive contains lfs files' do
+ let(:dummy_lfs_file_path) { File.join(shared.export_path, 'lfs-objects', 'dummy') }
+
+ def create_lfs_object_with_content(content)
+ dummy_lfs_file = Tempfile.new('existing')
+ File.write(dummy_lfs_file.path, content)
+ size = dummy_lfs_file.size
+ oid = LfsObject.calculate_oid(dummy_lfs_file.path)
+ LfsObject.create!(oid: oid, size: size, file: dummy_lfs_file)
+ end
+
+ before do
+ FileUtils.mkdir_p(File.dirname(dummy_lfs_file_path))
+ File.write(dummy_lfs_file_path, 'not very large')
+ allow(restorer).to receive(:lfs_file_paths).and_return([dummy_lfs_file_path])
+ end
+
+ it 'creates an lfs object for the project' do
+ expect { restorer.restore }.to change { project.reload.lfs_objects.size }.by(1)
+ end
+
+ it 'assigns the file correctly' do
+ restorer.restore
+
+ expect(project.lfs_objects.first.file.read).to eq('not very large')
+ end
+
+ it 'links an existing LFS object if it existed' do
+ lfs_object = create_lfs_object_with_content('not very large')
+
+ restorer.restore
+
+ expect(project.lfs_objects).to include(lfs_object)
+ end
+
+ it 'succeeds' do
+ expect(restorer.restore).to be_truthy
+ expect(shared.errors).to be_empty
+ end
+
+ it 'stores the upload' do
+ expect_any_instance_of(LfsObjectUploader).to receive(:store!)
+
+ restorer.restore
+ end
+ end
+
+ context 'without any LFS-objects' do
+ it 'succeeds' do
+ expect(restorer.restore).to be_truthy
+ expect(shared.errors).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
new file mode 100644
index 00000000000..9b0e21deb2e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::LfsSaver do
+ let(:shared) { project.import_export_shared }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:project) { create(:project) }
+
+ subject(:saver) { described_class.new(project: project, shared: shared) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ FileUtils.mkdir_p(shared.export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(shared.export_path)
+ end
+
+ describe '#save' do
+ context 'when the project has LFS objects locally stored' do
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'does not cause errors' do
+ saver.save
+
+ expect(shared.errors).to be_empty
+ end
+
+ it 'copies the file in the correct location when there is an lfs object' do
+ saver.save
+
+ expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
+ end
+ end
+
+ context 'when the LFS objects are stored in object storage' do
+ let(:lfs_object) { create(:lfs_object, :object_storage) }
+
+ before do
+ allow(LfsObjectUploader).to receive(:object_store_enabled?).and_return(true)
+ allow(lfs_object.file).to receive(:url).and_return('http://my-object-storage.local')
+ project.lfs_objects << lfs_object
+ end
+
+ it 'downloads the file to include in an archive' do
+ fake_uri = double
+ exported_file_path = "#{shared.export_path}/lfs-objects/#{lfs_object.oid}"
+
+ expect(fake_uri).to receive(:open).and_return(StringIO.new('LFS file content'))
+ expect(URI).to receive(:parse).with('http://my-object-storage.local').and_return(fake_uri)
+
+ saver.save
+
+ expect(File.read(exported_file_path)).to eq('LFS file content')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index f949a23ffbb..f84a777a27f 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -390,6 +390,7 @@ Service:
- default
- wiki_page_events
- confidential_issues_events
+- confidential_note_events
ProjectHook:
- id
- url
@@ -410,6 +411,7 @@ ProjectHook:
- token
- group_id
- confidential_issues_events
+- confidential_note_events
- repository_update_events
ProtectedBranch:
- id
diff --git a/spec/mailers/previews/email_rejection_mailer_preview.rb b/spec/mailers/previews/email_rejection_mailer_preview.rb
new file mode 100644
index 00000000000..639e8471232
--- /dev/null
+++ b/spec/mailers/previews/email_rejection_mailer_preview.rb
@@ -0,0 +1,5 @@
+class EmailRejectionMailerPreview < ActionMailer::Preview
+ def rejection
+ EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message
+ end
+end
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
index 43c3c89f140..e32fd0bd120 100644
--- a/spec/mailers/previews/notify_preview.rb
+++ b/spec/mailers/previews/notify_preview.rb
@@ -58,16 +58,89 @@ class NotifyPreview < ActionMailer::Preview
end
end
+ def closed_issue_email
+ Notify.closed_issue_email(user.id, issue.id, user.id).message
+ end
+
+ def issue_status_changed_email
+ Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
+ end
+
+ def closed_merge_request_email
+ Notify.closed_merge_request_email(user.id, issue.id, user.id).message
+ end
+
+ def merge_request_status_email
+ Notify.merge_request_status_email(user.id, merge_request.id, 'closed', user.id).message
+ end
+
+ def merged_merge_request_email
+ Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
+ end
+
+ def member_access_denied_email
+ Notify.member_access_denied_email('project', project.id, user.id).message
+ end
+
+ def member_access_granted_email
+ Notify.member_access_granted_email('project', user.id).message
+ end
+
+ def member_access_requested_email
+ Notify.member_access_requested_email('group', user.id, 'some@example.com').message
+ end
+
+ def member_invite_accepted_email
+ Notify.member_invite_accepted_email('project', user.id).message
+ end
+
+ def member_invite_declined_email
+ Notify.member_invite_declined_email(
+ 'project',
+ project.id,
+ 'invite@example.com',
+ user.id
+ ).message
+ end
+
+ def member_invited_email
+ Notify.member_invited_email('project', user.id, '1234').message
+ end
+
+ def pages_domain_enabled_email
+ cleanup do
+ pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now)
+
+ Notify.pages_domain_enabled_email(pages_domain, user).message
+ end
+ end
+
+ def pipeline_success_email
+ Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
+ end
+
+ def pipeline_failed_email
+ Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
+ end
+
private
def project
@project ||= Project.find_by_full_path('gitlab-org/gitlab-test')
end
+ def issue
+ @merge_request ||= project.issues.first
+ end
+
def merge_request
@merge_request ||= project.merge_requests.first
end
+ def pipeline
+ @pipeline = Ci::Pipeline.last
+ end
+
def user
@user ||= User.last
end
@@ -94,14 +167,4 @@ class NotifyPreview < ActionMailer::Preview
email
end
-
- def pipeline_success_email
- pipeline = Ci::Pipeline.last
- Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
- end
-
- def pipeline_failed_email
- pipeline = Ci::Pipeline.last
- Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
- end
end
diff --git a/spec/mailers/previews/repository_check_mailer_preview.rb b/spec/mailers/previews/repository_check_mailer_preview.rb
new file mode 100644
index 00000000000..19d4eab1805
--- /dev/null
+++ b/spec/mailers/previews/repository_check_mailer_preview.rb
@@ -0,0 +1,5 @@
+class RepositoryCheckMailerPreview < ActionMailer::Preview
+ def notify
+ RepositoryCheckMailer.notify(3).message
+ end
+end
diff --git a/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb
new file mode 100644
index 00000000000..4395e2f8264
--- /dev/null
+++ b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180122154930_schedule_set_confidential_note_events_on_services.rb')
+
+describe ScheduleSetConfidentialNoteEventsOnServices, :migration, :sidekiq do
+ let(:services_table) { table(:services) }
+ let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let!(:service_1) { services_table.create!(confidential_note_events: nil, note_events: true) }
+ let!(:service_2) { services_table.create!(confidential_note_events: nil, note_events: true) }
+ let!(:service_migrated) { services_table.create!(confidential_note_events: true, note_events: true) }
+ let!(:service_skip) { services_table.create!(confidential_note_events: nil, note_events: false) }
+ let!(:service_new) { services_table.create!(confidential_note_events: false, note_events: true) }
+ let!(:service_4) { services_table.create!(confidential_note_events: nil, note_events: true) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'schedules background migrations at correct time' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(20.minutes, service_1.id, service_1.id)
+ expect(migration_name).to be_scheduled_delayed_migration(40.minutes, service_2.id, service_2.id)
+ expect(migration_name).to be_scheduled_delayed_migration(60.minutes, service_4.id, service_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'correctly processes services' do
+ Sidekiq::Testing.inline! do
+ expect(services_table.where(confidential_note_events: nil).count).to eq 4
+ expect(services_table.where(confidential_note_events: true).count).to eq 1
+
+ migrate!
+
+ expect(services_table.where(confidential_note_events: nil).count).to eq 1
+ expect(services_table.where(confidential_note_events: true).count).to eq 4
+ end
+ end
+end
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
index c81ec887ded..df009cec25c 100644
--- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -4,8 +4,24 @@ require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_cluste
describe MigrateGcpClustersToNewClustersArchitectures, :migration do
let(:projects) { table(:projects) }
let(:project) { projects.create }
- let(:user) { create(:user) }
- let(:service) { create(:kubernetes_service, project_id: project.id) }
+ let(:users) { table(:users) }
+ let(:user) { users.create! }
+ let(:service) { GcpMigrationSpec::KubernetesService.create!(project_id: project.id) }
+
+ module GcpMigrationSpec
+ class KubernetesService < ActiveRecord::Base
+ self.table_name = 'services'
+
+ serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+
+ default_value_for :active, true
+ default_value_for :type, 'KubernetesService'
+ default_value_for :properties, {
+ api_url: 'https://kubernetes.example.com',
+ token: 'a' * 40
+ }
+ end
+ end
context 'when cluster is being created' do
let(:project_id) { project.id }
diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb
index cbc0ebeb44d..e2994103ed7 100644
--- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb
+++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb
@@ -7,9 +7,9 @@ describe RenameUsersWithRenamedNamespace, :delete do
other_user1 = create(:user, username: 'api0')
user = create(:user, username: "Users0")
- user.update_attribute(:username, 'Users')
+ user.update_column(:username, 'Users')
user1 = create(:user, username: "import0")
- user1.update_attribute(:username, 'import')
+ user1.update_column(:username, 'import')
described_class.new.up
diff --git a/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb
new file mode 100644
index 00000000000..027f4a91c90
--- /dev/null
+++ b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180104131052_schedule_set_confidential_note_events_on_webhooks.rb')
+
+describe ScheduleSetConfidentialNoteEventsOnWebhooks, :migration, :sidekiq do
+ let(:web_hooks_table) { table(:web_hooks) }
+ let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let!(:web_hook_1) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
+ let!(:web_hook_2) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
+ let!(:web_hook_migrated) { web_hooks_table.create!(confidential_note_events: true, note_events: true) }
+ let!(:web_hook_skip) { web_hooks_table.create!(confidential_note_events: nil, note_events: false) }
+ let!(:web_hook_new) { web_hooks_table.create!(confidential_note_events: false, note_events: true) }
+ let!(:web_hook_4) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'schedules background migrations at correct time' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, web_hook_1.id, web_hook_1.id)
+ expect(migration_name).to be_scheduled_delayed_migration(10.minutes, web_hook_2.id, web_hook_2.id)
+ expect(migration_name).to be_scheduled_delayed_migration(15.minutes, web_hook_4.id, web_hook_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'correctly processes web hooks' do
+ Sidekiq::Testing.inline! do
+ expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 4
+ expect(web_hooks_table.where(confidential_note_events: true).count).to eq 1
+
+ migrate!
+
+ expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 1
+ expect(web_hooks_table.where(confidential_note_events: true).count).to eq 4
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index c853f707e6d..86962cd8d61 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -191,6 +191,21 @@ describe Note do
end
end
+ describe "confidential?" do
+ it "delegates to noteable" do
+ issue_note = build(:note, :on_issue)
+ confidential_note = build(:note, noteable: create(:issue, confidential: true))
+
+ expect(issue_note.confidential?).to be_falsy
+ expect(confidential_note.confidential?).to be_truthy
+ end
+
+ it "is falsey when noteable can't be confidential" do
+ commit_note = build(:note_on_commit)
+ expect(commit_note.confidential?).to be_falsy
+ end
+ end
+
describe "cross_reference_not_visible_for?" do
let(:private_user) { create(:user) }
let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } }
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 3e2a166cdd6..0cd712e2f40 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -253,6 +253,21 @@ describe HipchatService do
"<b>#{title}</b>" \
"<pre>issue <strong>note</strong></pre>")
end
+
+ context 'with confidential issue' do
+ before do
+ issue.update!(confidential: true)
+ end
+
+ it 'calls Hipchat API with issue comment' do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ hipchat.execute(data)
+
+ message = hipchat.send(:create_message, data)
+
+ expect(message).to include("<pre>issue <strong>note</strong></pre>")
+ end
+ end
end
context 'when snippet comment event triggered' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0e560be9eaa..8bd62dcdccb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2022,6 +2022,22 @@ describe Project do
expect(forked_project.lfs_storage_project).to eq forked_project
end
end
+
+ describe '#all_lfs_objects' do
+ let(:lfs_object) { create(:lfs_object) }
+
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'returns the lfs object for a project' do
+ expect(project.all_lfs_objects).to contain_exactly(lfs_object)
+ end
+
+ it 'returns the lfs object for a fork' do
+ expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
+ end
+ end
end
describe '#pushes_since_gc' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 83ed3b203e6..28c908ea425 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -10,6 +10,22 @@ describe Service do
it { is_expected.to validate_presence_of(:type) }
end
+ describe 'Scopes' do
+ describe '.confidential_note_hooks' do
+ it 'includes services where confidential_note_events is true' do
+ create(:service, active: true, confidential_note_events: true)
+
+ expect(described_class.confidential_note_hooks.count).to eq 1
+ end
+
+ it 'excludes services where confidential_note_events is false' do
+ create(:service, active: true, confidential_note_events: false)
+
+ expect(described_class.confidential_note_hooks.count).to eq 0
+ end
+ end
+ end
+
describe "Test Button" do
describe '#can_test?' do
let(:service) { create(:service, project: project) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4027c420e47..a600987d0bf 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2071,6 +2071,8 @@ describe User do
expect(ghost).to be_ghost
expect(ghost).to be_persisted
+ expect(ghost.namespace).not_to be_nil
+ expect(ghost.namespace).to be_persisted
end
it "does not create a second ghost user if one is already present" do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 392cad667be..12a183fed1e 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -33,6 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
+ expect(json_response.first['confidential_note_events']).to eq(true)
expect(json_response.first['job_events']).to eq(true)
expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['wiki_page_events']).to eq(true)
@@ -62,6 +63,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
@@ -104,6 +106,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
+ expect(json_response['confidential_note_events']).to eq(nil)
expect(json_response['job_events']).to eq(true)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
@@ -152,6 +155,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 6ef5e93cb20..4e2ab919f0f 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -23,5 +23,23 @@ describe Notes::PostProcessService do
described_class.new(@note).execute
end
+
+ context 'with a confidential issue' do
+ let(:issue) { create(:issue, :confidential, project: project) }
+
+ it "doesn't call note hooks/services" do
+ expect(project).not_to receive(:execute_hooks).with(anything, :note_hooks)
+ expect(project).not_to receive(:execute_services).with(anything, :note_hooks)
+
+ described_class.new(@note).execute
+ end
+
+ it "calls confidential-note hooks/services" do
+ expect(project).to receive(:execute_hooks).with(anything, :confidential_note_hooks)
+ expect(project).to receive(:execute_services).with(anything, :confidential_note_hooks)
+
+ described_class.new(@note).execute
+ end
+ end
end
end
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 51491c7d529..f9e5530bc9d 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -8,6 +8,49 @@ describe Projects::ImportExport::ExportService do
let(:service) { described_class.new(project, user) }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+ it 'saves the version' do
+ expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the avatar' do
+ expect(Gitlab::ImportExport::AvatarSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the models' do
+ expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the uploads' do
+ expect(Gitlab::ImportExport::UploadsSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the repo' do
+ # once for the normal repo, once for the wiki
+ expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
+
+ service.execute
+ end
+
+ it 'saves the lfs objects' do
+ expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the wiki repo' do
+ expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
context 'when all saver services succeed' do
before do
allow(service).to receive(:save_services).and_return(true)
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index 5e1ce19eafb..07bc3a51fd8 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -4,6 +4,11 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
+ def execute_with_options(options)
+ receive(:new).with(webhook_url, options)
+ .and_return(double(:slack_service).as_null_object)
+ end
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -33,6 +38,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:project) { create(:project, :repository) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
+ let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
@@ -48,12 +54,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
WebMock.stub_request(:post, webhook_url)
- opts = {
- title: 'Awesome issue',
- description: 'please fix'
- }
-
- issue_service = Issues::CreateService.new(project, user, opts)
+ issue_service = Issues::CreateService.new(project, user, issue_service_options)
@issue = issue_service.execute
@issues_sample_data = issue_service.hook_data(@issue, 'open')
@@ -164,6 +165,26 @@ RSpec.shared_examples 'slack or mattermost notifications' do
chat_service.execute(@issues_sample_data)
end
+ context 'for confidential issues' do
+ let(:issue_service_options) { { title: 'Secret', confidential: true } }
+
+ it "uses confidential issue channel" do
+ chat_service.update_attributes(confidential_issue_channel: 'confidential')
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+
+ chat_service.execute(@issues_sample_data)
+ end
+
+ it 'falls back to issue channel' do
+ chat_service.update_attributes(issue_channel: 'fallback_channel')
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+
+ chat_service.execute(@issues_sample_data)
+ end
+ end
+
it "uses the right channel for wiki event" do
chat_service.update_attributes(wiki_page_channel: "random")
@@ -194,6 +215,32 @@ RSpec.shared_examples 'slack or mattermost notifications' do
chat_service.execute(note_data)
end
+
+ context 'for confidential notes' do
+ before do
+ issue_note.noteable.update!(confidential: true)
+ end
+
+ it "uses confidential channel" do
+ chat_service.update_attributes(confidential_note_channel: "confidential")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+
+ chat_service.execute(note_data)
+ end
+
+ it 'falls back to note channel' do
+ chat_service.update_attributes(note_channel: "fallback_channel")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+
+ chat_service.execute(note_data)
+ end
+ end
end
end
end
@@ -248,8 +295,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do
create(:note_on_issue, project: project, note: "issue note")
end
+ let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
+
it "calls Slack API for issue comment events" do
- data = Gitlab::DataBuilder::Note.build(issue_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 59e02fecbce..17d508d0146 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -62,10 +62,12 @@ describe ObjectStorage do
end
describe '#object_store' do
+ subject { uploader.object_store }
+
it "delegates to <mount>_store on model" do
expect(object).to receive(:file_store)
- uploader.object_store
+ subject
end
context 'when store is null' do
@@ -73,8 +75,36 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(nil)
end
- it "returns Store::LOCAL" do
- expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ context 'when object storage is enabled' do
+ context 'when direct uploads are enabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
+ end
+
+ it "uses Store::REMOTE" do
+ is_expected.to eq(described_class::Store::REMOTE)
+ end
+ end
+
+ context 'when direct uploads are disabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
+ end
+
+ it "uses Store::LOCAL" do
+ is_expected.to eq(described_class::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: false)
+ end
+
+ it "uses Store::LOCAL" do
+ is_expected.to eq(described_class::Store::LOCAL)
+ end
end
end
@@ -84,7 +114,7 @@ describe ObjectStorage do
end
it "returns the given value" do
- expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ is_expected.to eq(described_class::Store::REMOTE)
end
end
end