summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 12:10:29 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 12:10:29 +0000
commit5564275a0b378298dc6281599cbfe71a937109ff (patch)
treea468e1e60046356410219c35c23a8a428c5e2c5e
parentd87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff)
downloadgitlab-ce-5564275a0b378298dc6281599cbfe71a937109ff.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml27
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml1
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock1
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/boards/components/issue_count.vue2
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue43
-rw-r--r--app/assets/javascripts/code_navigation/components/popover.vue76
-rw-r--r--app/assets/javascripts/code_navigation/index.js20
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js62
-rw-r--r--app/assets/javascripts/code_navigation/store/index.js10
-rw-r--r--app/assets/javascripts/code_navigation/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/code_navigation/store/mutations.js23
-rw-r--r--app/assets/javascripts/code_navigation/store/state.js9
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js20
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js5
-rw-r--r--app/assets/javascripts/reports/components/modal.vue4
-rw-r--r--app/assets/javascripts/reports/constants.js2
-rw-r--r--app/assets/javascripts/reports/store/state.js2
-rw-r--r--app/assets/javascripts/user_popovers.js10
-rw-r--r--app/assets/stylesheets/framework/files.scss12
-rw-r--r--app/assets/stylesheets/utilities.scss7
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb4
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/models/deploy_token.rb33
-rw-r--r--app/models/group_deploy_token.rb23
-rw-r--r--app/models/project_deploy_token.rb4
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/user_bot_type_enums.rb12
-rw-r--r--app/models/user_preference.rb6
-rw-r--r--app/policies/base_policy.rb3
-rw-r--r--app/policies/concerns/policy_actor.rb4
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml9
-rw-r--r--app/views/profiles/preferences/update.js.erb8
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--changelogs/unreleased/13904-tab-width-option.yml5
-rw-r--r--changelogs/unreleased/197311-board-list-wip-limit-distorted-when-board-list-is-scoped-to-an-ass.yml5
-rw-r--r--changelogs/unreleased/21765-group-token-architecture.yml5
-rw-r--r--changelogs/unreleased/26247-junit-xml-tests-mis-present-the-test-execution-time-from-test-cases.yml5
-rw-r--r--changelogs/unreleased/37012-jira-dvcs-error.yml5
-rw-r--r--changelogs/unreleased/39474-unable-to-view-project-audit-events-statement-timeouts.yml5
-rw-r--r--changelogs/unreleased/fix-duplicated-user-popover.yml5
-rw-r--r--changelogs/unreleased/fix-quoted-printable-unicode-in-mails.yml5
-rw-r--r--config/initializers/mail_encoding_patch.rb82
-rw-r--r--db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb17
-rw-r--r--db/migrate/20191218190253_add_tab_width_to_user_preferences.rb9
-rw-r--r--db/migrate/20200121200203_create_group_deploy_tokens.rb16
-rw-r--r--db/migrate/20200129172428_add_index_on_audit_events_id_desc.rb24
-rw-r--r--db/schema.rb15
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/topics/autodevops/index.md33
-rw-r--r--doc/user/application_security/dependency_list/index.md2
-rw-r--r--doc/user/application_security/sast/index.md6
-rw-r--r--doc/user/profile/preferences.md9
-rw-r--r--doc/user/project/operations/error_tracking.md2
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/email/hook/smime_signature_interceptor.rb1
-rw-r--r--lib/gitlab/tab_width.rb29
-rw-r--r--locale/gitlab.pot17
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb11
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb1
-rw-r--r--spec/factories/deploy_tokens.rb9
-rw-r--r--spec/factories/group_deploy_tokens.rb8
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/groups/navbar_spec.rb99
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb27
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb4
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb1
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap39
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js64
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js58
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js221
-rw-r--r--spec/frontend/code_navigation/store/mutations_spec.js63
-rw-r--r--spec/frontend/code_navigation/utils/index_spec.js58
-rw-r--r--spec/initializers/mail_encoding_patch_spec.rb207
-rw-r--r--spec/javascripts/reports/components/modal_spec.js4
-rw-r--r--spec/javascripts/user_popovers_spec.js7
-rw-r--r--spec/lib/gitlab/auth_spec.rb18
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb17
-rw-r--r--spec/lib/gitlab/tab_width_spec.rb31
-rw-r--r--spec/models/deploy_token_spec.rb170
-rw-r--r--spec/models/group_deploy_token_spec.rb17
-rw-r--r--spec/models/user_preference_spec.rb15
-rw-r--r--spec/models/user_spec.rb40
-rw-r--r--spec/policies/project_policy_spec.rb14
96 files changed, 1938 insertions, 95 deletions
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index 9d80f4cba94..6c52afb068f 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -2,6 +2,32 @@
.if-canonical-dot-com-gitlab-org-group-master-refs: &if-canonical-dot-com-gitlab-org-group-master-refs
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_COMMIT_REF_NAME == "master"'
+# Make sure to update all the similar patterns in other CI config files if you modify these patterns
+.code-backstage-qa-patterns: &code-backstage-qa-patterns
+ - ".gitlab/ci/**/*"
+ - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
+ - ".csscomb.json"
+ - "Dockerfile.assets"
+ - "*_VERSION"
+ - "Gemfile{,.lock}"
+ - "Rakefile"
+ - "{babel.config,jest.config}.js"
+ - "config.ru"
+ - "{package.json,yarn.lock}"
+ - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/reference/*" # Files in this folder are auto-generated
+ # Backstage changes
+ - "Dangerfile"
+ - "danger/**/*"
+ - "{,ee/}fixtures/**/*"
+ - "{,ee/}rubocop/**/*"
+ - "{,ee/}spec/**/*"
+ - "doc/README.md" # Some RSpec test rely on this file
+ # QA changes
+ - ".dockerignore"
+ - "qa/**/*"
+
pages:
extends:
- .default-tags
@@ -9,6 +35,7 @@ pages:
- .default-cache
rules:
- <<: *if-canonical-dot-com-gitlab-org-group-master-refs
+ changes: *code-backstage-qa-patterns
when: on_success
stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 6b0a7f31f1a..3fe8411ccad 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -68,6 +68,7 @@ setup-test-env:
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
+ - junit_rspec.xml
reports:
junit: junit_rspec.xml
diff --git a/Gemfile b/Gemfile
index c9319a34594..18da93d2a05 100644
--- a/Gemfile
+++ b/Gemfile
@@ -488,3 +488,8 @@ gem 'liquid', '~> 4.0'
gem 'lru_redux'
gem 'erubi', '~> 1.9.0'
+
+# Locked as long as quoted-printable encoding issues are not resolved
+# Monkey-patched in `config/initializers/mail_encoding_patch.rb`
+# See https://gitlab.com/gitlab-org/gitlab/issues/197386
+gem 'mail', '= 2.7.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index c8912d70810..64e701d22de 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1283,6 +1283,7 @@ DEPENDENCIES
lograge (~> 0.5)
loofah (~> 2.2)
lru_redux
+ mail (= 2.7.1)
mail_room (~> 0.10.0)
marginalia (~> 1.8.0)
memory_profiler (~> 0.9)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6de9ab9efb3..76f3020c5c2 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -45,6 +45,7 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
+ lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -457,6 +458,14 @@ const Api = {
return axios.get(url);
},
+ lsifData(projectPath, commitId, path) {
+ const url = Api.buildUrl(this.lsifPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':commit_id', commitId);
+
+ return axios.get(url, { params: { path } });
+ },
+
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
diff --git a/app/assets/javascripts/boards/components/issue_count.vue b/app/assets/javascripts/boards/components/issue_count.vue
index c50a3c1c0d3..d55f7151d7e 100644
--- a/app/assets/javascripts/boards/components/issue_count.vue
+++ b/app/assets/javascripts/boards/components/issue_count.vue
@@ -25,7 +25,7 @@ export default {
</script>
<template>
- <div class="issue-count">
+ <div class="issue-count text-nowrap">
<span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }">
{{ issuesSize }}
</span>
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue
new file mode 100644
index 00000000000..0e5f1f0485d
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/components/app.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import Popover from './popover.vue';
+
+export default {
+ components: {
+ Popover,
+ },
+ computed: {
+ ...mapState(['currentDefinition', 'currentDefinitionPosition']),
+ },
+ mounted() {
+ this.blobViewer = document.querySelector('.blob-viewer');
+
+ this.addGlobalEventListeners();
+ this.fetchData();
+ },
+ beforeDestroy() {
+ this.removeGlobalEventListeners();
+ },
+ methods: {
+ ...mapActions(['fetchData', 'showDefinition']),
+ addGlobalEventListeners() {
+ if (this.blobViewer) {
+ this.blobViewer.addEventListener('click', this.showDefinition);
+ }
+ },
+ removeGlobalEventListeners() {
+ if (this.blobViewer) {
+ this.blobViewer.removeEventListener('click', this.showDefinition);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <popover
+ v-if="currentDefinition"
+ :position="currentDefinitionPosition"
+ :data="currentDefinition"
+ />
+</template>
diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue
new file mode 100644
index 00000000000..d5bbe430fcd
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/components/popover.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ position: {
+ type: Object,
+ required: true,
+ },
+ data: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ offsetLeft: 0,
+ };
+ },
+ computed: {
+ positionStyles() {
+ return {
+ left: `${this.position.x - this.offsetLeft}px`,
+ top: `${this.position.y + this.position.height}px`,
+ };
+ },
+ },
+ watch: {
+ position: {
+ handler() {
+ this.$nextTick(() => this.updateOffsetLeft());
+ },
+ deep: true,
+ immediate: true,
+ },
+ },
+ methods: {
+ updateOffsetLeft() {
+ this.offsetLeft = Math.max(
+ 0,
+ this.$el.offsetLeft + this.$el.offsetWidth - window.innerWidth + 20,
+ );
+ },
+ },
+ colorScheme: gon?.user_color_scheme,
+};
+</script>
+
+<template>
+ <div
+ :style="positionStyles"
+ class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
+ >
+ <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div>
+ <div v-for="(hover, index) in data.hover" :key="index" class="border-bottom">
+ <pre
+ v-if="hover.language"
+ ref="code-output"
+ :class="$options.colorScheme"
+ class="border-0 bg-transparent m-0 code highlight"
+ v-html="hover.value"
+ ></pre>
+ <p v-else ref="doc-output" class="p-3 m-0">
+ {{ hover.value }}
+ </p>
+ </div>
+ <div v-if="data.definition_url" class="popover-body">
+ <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default">
+ {{ __('Go to definition') }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js
new file mode 100644
index 00000000000..2222c986dfe
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import store from './store';
+import App from './components/app.vue';
+
+Vue.use(Vuex);
+
+export default () => {
+ const el = document.getElementById('js-code-navigation');
+
+ store.dispatch('setInitialData', el.dataset);
+
+ return new Vue({
+ el,
+ store,
+ render(h) {
+ return h(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
new file mode 100644
index 00000000000..10483abfb23
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -0,0 +1,62 @@
+import api from '~/api';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import * as types from './mutation_types';
+import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
+
+export default {
+ setInitialData({ commit }, data) {
+ commit(types.SET_INITIAL_DATA, data);
+ },
+ requestDataError({ commit }) {
+ commit(types.REQUEST_DATA_ERROR);
+ createFlash(__('An error occurred loading code navigation'));
+ },
+ fetchData({ commit, dispatch, state }) {
+ commit(types.REQUEST_DATA);
+
+ api
+ .lsifData(state.projectPath, state.commitId, state.path)
+ .then(({ data }) => {
+ const normalizedData = data.reduce((acc, d) => {
+ if (d.hover) {
+ acc[`${d.start_line}:${d.start_char}`] = d;
+ addInteractionClass(d);
+ }
+ return acc;
+ }, {});
+
+ commit(types.REQUEST_DATA_SUCCESS, normalizedData);
+ })
+ .catch(() => dispatch('requestDataError'));
+ },
+ showDefinition({ commit, state }, { target: el }) {
+ let definition;
+ let position;
+
+ if (!state.data) return;
+
+ const isCurrentElementPopoverOpen = el.classList.contains('hll');
+
+ if (getCurrentHoverElement()) {
+ getCurrentHoverElement().classList.remove('hll');
+ }
+
+ if (el.classList.contains('js-code-navigation') && !isCurrentElementPopoverOpen) {
+ const { lineIndex, charIndex } = el.dataset;
+
+ position = {
+ x: el.offsetLeft,
+ y: el.offsetTop,
+ height: el.offsetHeight,
+ };
+ definition = state.data[`${lineIndex}:${charIndex}`];
+
+ el.classList.add('hll');
+
+ setCurrentHoverElement(el);
+ }
+
+ commit(types.SET_CURRENT_DEFINITION, { definition, position });
+ },
+};
diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js
new file mode 100644
index 00000000000..fe48f3ac7f5
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/store/index.js
@@ -0,0 +1,10 @@
+import Vuex from 'vuex';
+import createState from './state';
+import actions from './actions';
+import mutations from './mutations';
+
+export default new Vuex.Store({
+ actions,
+ mutations,
+ state: createState(),
+});
diff --git a/app/assets/javascripts/code_navigation/store/mutation_types.js b/app/assets/javascripts/code_navigation/store/mutation_types.js
new file mode 100644
index 00000000000..29a2897a6fd
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/store/mutation_types.js
@@ -0,0 +1,5 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+export const REQUEST_DATA = 'REQUEST_DATA';
+export const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS';
+export const REQUEST_DATA_ERROR = 'REQUEST_DATA_ERROR';
+export const SET_CURRENT_DEFINITION = 'SET_CURRENT_DEFINITION';
diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js
new file mode 100644
index 00000000000..bb833a5adbc
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/store/mutations.js
@@ -0,0 +1,23 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) {
+ state.projectPath = projectPath;
+ state.commitId = commitId;
+ state.blobPath = blobPath;
+ },
+ [types.REQUEST_DATA](state) {
+ state.loading = true;
+ },
+ [types.REQUEST_DATA_SUCCESS](state, data) {
+ state.loading = false;
+ state.data = data;
+ },
+ [types.REQUEST_DATA_ERROR](state) {
+ state.loading = false;
+ },
+ [types.SET_CURRENT_DEFINITION](state, { definition, position }) {
+ state.currentDefinition = definition;
+ state.currentDefinitionPosition = position;
+ },
+};
diff --git a/app/assets/javascripts/code_navigation/store/state.js b/app/assets/javascripts/code_navigation/store/state.js
new file mode 100644
index 00000000000..a7b3b289db4
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/store/state.js
@@ -0,0 +1,9 @@
+export default () => ({
+ projectPath: null,
+ commitId: null,
+ blobPath: null,
+ loading: false,
+ data: null,
+ currentDefinition: null,
+ currentDefinitionPosition: null,
+});
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
new file mode 100644
index 00000000000..2dee0de6501
--- /dev/null
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -0,0 +1,20 @@
+export const cachedData = new Map();
+
+export const getCurrentHoverElement = () => cachedData.get('current');
+export const setCurrentHoverElement = el => cachedData.set('current', el);
+
+export const addInteractionClass = d => {
+ let charCount = 0;
+ const line = document.getElementById(`LC${d.start_line + 1}`);
+ const el = [...line.childNodes].find(({ textContent }) => {
+ if (charCount === d.start_char) return true;
+ charCount += textContent.length;
+ return false;
+ });
+
+ if (el) {
+ el.setAttribute('data-char-index', d.start_char);
+ el.setAttribute('data-line-index', d.start_line);
+ el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');
+ }
+};
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index aee67899ca2..caf9a8c0b64 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -30,4 +30,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
GpgBadges.fetch();
+
+ if (gon.features?.codeNavigation) {
+ // eslint-disable-next-line promise/catch-or-return
+ import('~/code_navigation').then(m => m.default());
+ }
});
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
index 40ce200befb..78c355ecb76 100644
--- a/app/assets/javascripts/reports/components/modal.vue
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -46,8 +46,8 @@ export default {
</a>
</template>
- <template v-else-if="field.type === $options.fieldTypes.miliseconds">{{
- sprintf(__('%{value} ms'), { value: field.value })
+ <template v-else-if="field.type === $options.fieldTypes.seconds">{{
+ sprintf(__('%{value} s'), { value: field.value })
}}</template>
<template v-else-if="field.type === $options.fieldTypes.text">
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index 66ac1af062b..1845b51e6b2 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -1,7 +1,7 @@
export const fieldTypes = {
codeBock: 'codeBlock',
link: 'link',
- miliseconds: 'miliseconds',
+ seconds: 'seconds',
text: 'text',
};
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 25f9f70d095..d0b2d0a37f5 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -48,7 +48,7 @@ export default () => ({
execution_time: {
value: null,
text: s__('Reports|Execution time'),
- type: fieldTypes.miliseconds,
+ type: fieldTypes.seconds,
},
failure: {
value: null,
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 5b9e3817f3a..67e5f175039 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -54,11 +54,17 @@ const populateUserInfo = user => {
);
};
+const initializedPopovers = new Map();
+
export default (elements = document.querySelectorAll('.js-user-link')) => {
const userLinks = Array.from(elements);
+ const UserPopoverComponent = Vue.extend(UserPopover);
return userLinks.map(el => {
- const UserPopoverComponent = Vue.extend(UserPopover);
+ if (initializedPopovers.has(el)) {
+ return initializedPopovers.get(el);
+ }
+
const user = {
location: null,
bio: null,
@@ -73,6 +79,8 @@ export default (elements = document.querySelectorAll('.js-user-link')) => {
},
});
+ initializedPopovers.set(el, renderedPopover);
+
renderedPopover.$mount();
el.addEventListener('mouseenter', ({ target }) => {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 1a017f03ebb..bb1c304b9fe 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -499,3 +499,15 @@ span.idiff {
background-color: transparent;
border: transparent;
}
+
+.code-navigation {
+ border-bottom: 1px $gray-darkest dashed;
+
+ &:hover {
+ border-bottom-color: $almost-black;
+ }
+}
+
+.code-navigation-popover {
+ max-width: 450px;
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index f3e6927767c..0fd6aafef0d 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -28,6 +28,13 @@
}
}
+@for $i from 1 through 12 {
+ #{'.tab-width-#{$i}'} {
+ -moz-tab-size: $i;
+ tab-size: $i;
+ }
+}
+
.border-width-1px { border-width: 1px; }
.border-bottom-width-1px { border-bottom-width: 1px; }
.border-style-dashed { border-style: dashed; }
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 2166dd7dad7..1477d79c911 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:time_display_relative,
:time_format_in_24h,
:show_whitespace_in_diffs,
+ :tab_width,
:sourcegraph_enabled,
:render_whitespace_in_code
]
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 3cd14cf845f..01e5103198b 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -29,6 +29,10 @@ class Projects::BlobController < Projects::ApplicationController
before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update]
+ before_action only: :show do
+ push_frontend_feature_flag(:code_navigation, @project)
+ end
+
def new
commit unless @repository.empty?
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 6a271e93cd9..8a79217c929 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -63,6 +63,10 @@ module PreferencesHelper
Gitlab::ColorSchemes.for_user(current_user).css_class
end
+ def user_tab_width
+ Gitlab::TabWidth.css_class_for_user(current_user)
+ end
+
def language_choices
Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 011871f373f..93c38d2f933 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -706,6 +706,10 @@ module ProjectsHelper
Feature.enabled?(:vue_file_list, @project)
end
+ def native_code_navigation_enabled?(project)
+ Feature.enabled?(:code_navigation, project)
+ end
+
def show_visibility_confirm_modal?(project)
project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 3d098406ab1..31c813edb67 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -15,6 +15,11 @@ class DeployToken < ApplicationRecord
has_many :project_deploy_tokens, inverse_of: :deploy_token
has_many :projects, through: :project_deploy_tokens
+ has_many :group_deploy_tokens, inverse_of: :deploy_token
+ has_many :groups, through: :group_deploy_tokens
+
+ validate :no_groups, unless: :group_type?
+ validate :no_projects, unless: :project_type?
validate :ensure_at_least_one_scope
validates :username,
length: { maximum: 255 },
@@ -24,6 +29,7 @@ class DeployToken < ApplicationRecord
message: "can contain only letters, digits, '_', '-', '+', and '.'"
}
+ validates :deploy_token_type, presence: true
enum deploy_token_type: {
group_type: 1,
project_type: 2
@@ -56,18 +62,31 @@ class DeployToken < ApplicationRecord
end
def has_access_to?(requested_project)
- active? && project == requested_project
+ return false unless active?
+ return false unless holder
+
+ holder.has_access_to?(requested_project)
end
# This is temporal. Currently we limit DeployToken
- # to a single project, later we're going to extend
- # that to be for multiple projects and namespaces.
+ # to a single project or group, later we're going to
+ # extend that to be for multiple projects and namespaces.
def project
strong_memoize(:project) do
projects.first
end
end
+ def holder
+ strong_memoize(:holder) do
+ if project_type?
+ project_deploy_tokens.first
+ elsif group_type?
+ group_deploy_tokens.first
+ end
+ end
+ end
+
def expires_at
expires_at = read_attribute(:expires_at)
expires_at != Forever.date ? expires_at : nil
@@ -92,4 +111,12 @@ class DeployToken < ApplicationRecord
def default_username
"gitlab+deploy-token-#{id}" if persisted?
end
+
+ def no_groups
+ errors.add(:deploy_token, 'cannot have groups assigned') if group_deploy_tokens.any?
+ end
+
+ def no_projects
+ errors.add(:deploy_token, 'cannot have projects assigned') if project_deploy_tokens.any?
+ end
end
diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb
new file mode 100644
index 00000000000..221a7d768ae
--- /dev/null
+++ b/app/models/group_deploy_token.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class GroupDeployToken < ApplicationRecord
+ belongs_to :group, class_name: '::Group'
+ belongs_to :deploy_token, inverse_of: :group_deploy_tokens
+
+ validates :deploy_token, presence: true
+ validates :group, presence: true
+ validates :deploy_token_id, uniqueness: { scope: [:group_id] }
+
+ def has_access_to?(requested_project)
+ return false unless Feature.enabled?(:allow_group_deploy_token, default: true)
+
+ requested_project_group = requested_project&.group
+ return false unless requested_project_group
+ return true if requested_project_group.id == group_id
+
+ requested_project_group
+ .ancestors
+ .where(id: group_id)
+ .exists?
+ end
+end
diff --git a/app/models/project_deploy_token.rb b/app/models/project_deploy_token.rb
index a55667496fb..0bce1c745f7 100644
--- a/app/models/project_deploy_token.rb
+++ b/app/models/project_deploy_token.rb
@@ -7,4 +7,8 @@ class ProjectDeployToken < ApplicationRecord
validates :deploy_token, presence: true
validates :project, presence: true
validates :deploy_token_id, uniqueness: { scope: [:project_id] }
+
+ def has_access_to?(requested_project)
+ requested_project == project
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index a5ef03215d3..3512e663f4a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -59,6 +59,8 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
+ enum bot_type: ::UserBotTypeEnums.bots
+
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
@@ -246,6 +248,7 @@ class User < ApplicationRecord
delegate :time_display_relative, :time_display_relative=, to: :user_preference
delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
+ delegate :tab_width, :tab_width=, to: :user_preference
delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
delegate :setup_for_company, :setup_for_company=, to: :user_preference
delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference
@@ -322,6 +325,8 @@ class User < ApplicationRecord
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
+ scope :bots, -> { where.not(bot_type: nil) }
+ scope :humans, -> { where(bot_type: nil) }
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
where('EXISTS (?)',
@@ -598,6 +603,15 @@ class User < ApplicationRecord
end
end
+ def alert_bot
+ email_pattern = "alert%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(bot_type: :alert_bot), 'alert-bot', email_pattern) do |u|
+ u.bio = 'The GitLab alert bot'
+ u.name = 'GitLab Alert Bot'
+ end
+ end
+
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
@@ -613,16 +627,20 @@ class User < ApplicationRecord
username
end
+ def bot?
+ bot_type.present?
+ end
+
def internal?
- ghost?
+ ghost? || bot?
end
def self.internal
- where(ghost: true)
+ where(ghost: true).or(bots)
end
def self.non_internal
- without_ghosts
+ without_ghosts.humans
end
#
diff --git a/app/models/user_bot_type_enums.rb b/app/models/user_bot_type_enums.rb
new file mode 100644
index 00000000000..b6b08ce650b
--- /dev/null
+++ b/app/models/user_bot_type_enums.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module UserBotTypeEnums
+ def self.bots
+ # When adding a new key, please ensure you are not conflicting with EE-only keys in app/models/user_bot_types_enums.rb
+ {
+ alert_bot: 2
+ }
+ end
+end
+
+UserBotTypeEnums.prepend_if_ee('EE::UserBotTypeEnums')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 713b0598029..48a56cded0e 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -9,7 +9,13 @@ class UserPreference < ApplicationRecord
belongs_to :user
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
+ validates :tab_width, numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: Gitlab::TabWidth::MIN,
+ less_than_or_equal_to: Gitlab::TabWidth::MAX
+ }
+ default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
default_value_for :time_display_relative, value: true, allows_nil: false
default_value_for :time_format_in_24h, value: false, allows_nil: false
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index c93a19bdc3d..ce3e5b0195c 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -44,6 +44,9 @@ class BasePolicy < DeclarativePolicy::Base
::Gitlab::ExternalAuthorization.perform_check?
end
+ with_options scope: :user, score: 0
+ condition(:alert_bot) { @user&.alert_bot? }
+
rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
prevent :read_cross_project
end
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index b963a64b429..406677d7b56 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -33,6 +33,10 @@ module PolicyActor
def can_create_group
false
end
+
+ def alert_bot?
+ false
+ end
end
PolicyActor.prepend_if_ee('EE::PolicyActor')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index bbcb3c637a9..ee22a2d84e7 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -515,6 +515,8 @@ class ProjectPolicy < BasePolicy
end
def lookup_access_level!
+ return ::Gitlab::Access::REPORTER if alert_bot?
+
# NOTE: max_member_access has its own cache
project.team.max_member_access(@user.id)
end
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 7af190f5a0b..eb58115451d 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -4,7 +4,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
+ %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} #{client_class_list}", data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
= render "layouts/init_client_detection_flags"
= render 'peek/bar'
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index 91a7777514c..8d0775f6f27 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
+ %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 93acd6f550b..12d42ce9892 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -69,6 +69,15 @@
= f.check_box :show_whitespace_in_diffs, class: 'form-check-input'
= f.label :show_whitespace_in_diffs, class: 'form-check-label' do
= s_('Preferences|Show whitespace changes in diffs')
+ .form-group
+ = f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
+ = f.number_field :tab_width,
+ class: 'form-control',
+ min: Gitlab::TabWidth::MIN,
+ max: Gitlab::TabWidth::MAX,
+ required: true
+ .form-text.text-muted
+ = s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX }
.col-sm-12
%hr
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 8966dd3fd86..8397acbf1b3 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -12,5 +12,9 @@ if ('<%= current_user.layout %>' === 'fluid') {
// Re-enable the "Save" button
$('input[type=submit]').enable()
-// Show the notice flash message
-new Flash('<%= flash.discard(:notice) %>', 'notice')
+// Show flash messages
+<% if flash.notice %>
+ new Flash('<%= flash.discard(:notice) %>', 'notice')
+<% elsif flash.alert %>
+ new Flash('<%= flash.discard(:alert) %>', 'alert')
+<% end %>
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index cf273aab108..9803d65c4fb 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -9,6 +9,8 @@
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
+ - if native_code_navigation_enabled?(@project)
+ #js-code-navigation{ data: { commit_id: blob.commit_id, path: blob.path, project_path: @project.full_path } }
%article.file-holder
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
diff --git a/changelogs/unreleased/13904-tab-width-option.yml b/changelogs/unreleased/13904-tab-width-option.yml
new file mode 100644
index 00000000000..eaa3dae7deb
--- /dev/null
+++ b/changelogs/unreleased/13904-tab-width-option.yml
@@ -0,0 +1,5 @@
+---
+title: Add tab width option to user preferences
+merge_request: 22063
+author: Alexander Oleynikov
+type: added
diff --git a/changelogs/unreleased/197311-board-list-wip-limit-distorted-when-board-list-is-scoped-to-an-ass.yml b/changelogs/unreleased/197311-board-list-wip-limit-distorted-when-board-list-is-scoped-to-an-ass.yml
new file mode 100644
index 00000000000..426830ae083
--- /dev/null
+++ b/changelogs/unreleased/197311-board-list-wip-limit-distorted-when-board-list-is-scoped-to-an-ass.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issue count wrapping on board list
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/21765-group-token-architecture.yml b/changelogs/unreleased/21765-group-token-architecture.yml
new file mode 100644
index 00000000000..8db4724e3b6
--- /dev/null
+++ b/changelogs/unreleased/21765-group-token-architecture.yml
@@ -0,0 +1,5 @@
+---
+title: Update deploy token architecture to introduce group-level deploy tokens.
+merge_request: 23460
+author:
+type: added
diff --git a/changelogs/unreleased/26247-junit-xml-tests-mis-present-the-test-execution-time-from-test-cases.yml b/changelogs/unreleased/26247-junit-xml-tests-mis-present-the-test-execution-time-from-test-cases.yml
new file mode 100644
index 00000000000..4b3d514e764
--- /dev/null
+++ b/changelogs/unreleased/26247-junit-xml-tests-mis-present-the-test-execution-time-from-test-cases.yml
@@ -0,0 +1,5 @@
+---
+title: Label MR test modal execution time as seconds
+merge_request: 24019
+author:
+type: fixed
diff --git a/changelogs/unreleased/37012-jira-dvcs-error.yml b/changelogs/unreleased/37012-jira-dvcs-error.yml
new file mode 100644
index 00000000000..a00ce632020
--- /dev/null
+++ b/changelogs/unreleased/37012-jira-dvcs-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix JIRA DVCS retrieving repositories
+merge_request: 23180
+author:
+type: fixed
diff --git a/changelogs/unreleased/39474-unable-to-view-project-audit-events-statement-timeouts.yml b/changelogs/unreleased/39474-unable-to-view-project-audit-events-statement-timeouts.yml
new file mode 100644
index 00000000000..f128eb4e417
--- /dev/null
+++ b/changelogs/unreleased/39474-unable-to-view-project-audit-events-statement-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to audit_events (entity_id, entity_type, id)
+merge_request: 23998
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-duplicated-user-popover.yml b/changelogs/unreleased/fix-duplicated-user-popover.yml
new file mode 100644
index 00000000000..fafce90124f
--- /dev/null
+++ b/changelogs/unreleased/fix-duplicated-user-popover.yml
@@ -0,0 +1,5 @@
+---
+title: Fix duplicated user popovers
+merge_request: 24405
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-quoted-printable-unicode-in-mails.yml b/changelogs/unreleased/fix-quoted-printable-unicode-in-mails.yml
new file mode 100644
index 00000000000..50010b0b4ac
--- /dev/null
+++ b/changelogs/unreleased/fix-quoted-printable-unicode-in-mails.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quoted-printable encoding for unicode and newlines in mails
+merge_request: 24153
+author: Diego Louzán
+type: fixed
diff --git a/config/initializers/mail_encoding_patch.rb b/config/initializers/mail_encoding_patch.rb
new file mode 100644
index 00000000000..d53b058ba75
--- /dev/null
+++ b/config/initializers/mail_encoding_patch.rb
@@ -0,0 +1,82 @@
+# Monkey patch mail 2.7.1 to fix quoted-printable issues with newlines
+# The issues upstream invalidate SMIME signatures under some conditions
+# This was working properly in 2.6.6
+#
+# See https://gitlab.com/gitlab-org/gitlab/issues/197386
+# See https://github.com/mikel/mail/issues/1190
+
+module Mail
+ module Encodings
+ # PATCH
+ # This reverts https://github.com/mikel/mail/pull/1113, which solves some
+ # encoding issues with binary attachments encoded in quoted-printable, but
+ # unfortunately breaks re-encoding of messages
+ class QuotedPrintable < SevenBit
+ def self.decode(str)
+ ::Mail::Utilities.to_lf str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack1("M*")
+ end
+
+ def self.encode(str)
+ ::Mail::Utilities.to_crlf([::Mail::Utilities.to_lf(str)].pack("M"))
+ end
+ end
+ end
+
+ class Body
+ def encoded(transfer_encoding = nil, charset = nil)
+ # PATCH
+ # Use provided parameter charset (from parent Message) if not nil,
+ # otherwise use own self.charset
+ # Required because the Message potentially has on its headers the charset
+ # that needs to be used (e.g. 'Content-Type: text/plain; charset=UTF-8')
+ charset = self.charset if charset.nil?
+
+ if multipart?
+ self.sort_parts!
+ encoded_parts = parts.map { |p| p.encoded }
+ ([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
+ else
+ dec = Mail::Encodings.get_encoding(encoding)
+ enc = if Utilities.blank?(transfer_encoding)
+ dec
+ else
+ negotiate_best_encoding(transfer_encoding)
+ end
+
+ if dec.nil?
+ # Cannot decode, so skip normalization
+ raw_source
+ else
+ # Decode then encode to normalize and allow transforming
+ # from base64 to Q-P and vice versa
+ decoded = dec.decode(raw_source)
+
+ if defined?(Encoding) && charset && charset != "US-ASCII"
+ # PATCH
+ # We need to force the encoding: in the case of quoted-printable
+ # this will throw an exception otherwise, because `decoded` will have
+ # an encoding of BINARY (or its equivalent ASCII-8BIT),
+ # coming from QuotedPrintable#decode, and inside it from String#unpack1
+ decoded = decoded.force_encoding(charset)
+ decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
+ end
+
+ enc.encode(decoded)
+ end
+ end
+ end
+ end
+
+ class Message
+ def encoded
+ ready_to_send!
+ buffer = header.encoded
+ buffer << "\r\n"
+ # PATCH
+ # Pass the Message charset down to the contained Body, the headers
+ # potentially contain the charset needed to be applied
+ buffer << body.encoded(content_transfer_encoding, charset)
+ buffer
+ end
+ end
+end
diff --git a/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb b/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb
new file mode 100644
index 00000000000..6cd32cdcfe9
--- /dev/null
+++ b/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddSamlProviderProhibitedOuterForks < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :saml_providers, :prohibited_outer_forks, :boolean, default: false, allow_null: true
+ end
+
+ def down
+ remove_column :saml_providers, :prohibited_outer_forks
+ end
+end
diff --git a/db/migrate/20191218190253_add_tab_width_to_user_preferences.rb b/db/migrate/20191218190253_add_tab_width_to_user_preferences.rb
new file mode 100644
index 00000000000..b03dd8f76b9
--- /dev/null
+++ b/db/migrate/20191218190253_add_tab_width_to_user_preferences.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddTabWidthToUserPreferences < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column(:user_preferences, :tab_width, :integer, limit: 2)
+ end
+end
diff --git a/db/migrate/20200121200203_create_group_deploy_tokens.rb b/db/migrate/20200121200203_create_group_deploy_tokens.rb
new file mode 100644
index 00000000000..55b30745fcf
--- /dev/null
+++ b/db/migrate/20200121200203_create_group_deploy_tokens.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateGroupDeployTokens < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :group_deploy_tokens do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :group, index: false, null: false, foreign_key: { to_table: :namespaces, on_delete: :cascade }
+ t.references :deploy_token, null: false, foreign_key: { on_delete: :cascade }
+
+ t.index [:group_id, :deploy_token_id], unique: true, name: 'index_group_deploy_tokens_on_group_and_deploy_token_ids'
+ end
+ end
+end
diff --git a/db/migrate/20200129172428_add_index_on_audit_events_id_desc.rb b/db/migrate/20200129172428_add_index_on_audit_events_id_desc.rb
new file mode 100644
index 00000000000..b9182c99ebf
--- /dev/null
+++ b/db/migrate/20200129172428_add_index_on_audit_events_id_desc.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddIndexOnAuditEventsIdDesc < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ OLD_INDEX_NAME = 'index_audit_events_on_entity_id_and_entity_type'
+ NEW_INDEX_NAME = 'index_audit_events_on_entity_id_and_entity_type_and_id_desc'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :audit_events, [:entity_id, :entity_type, :id], name: NEW_INDEX_NAME,
+ order: { entity_id: :asc, entity_type: :asc, id: :desc }
+
+ remove_concurrent_index_by_name :audit_events, OLD_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :audit_events, [:entity_id, :entity_type], name: OLD_INDEX_NAME
+
+ remove_concurrent_index_by_name :audit_events, NEW_INDEX_NAME
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1c96e4eefcf..1050b265acb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -465,7 +465,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.datetime "created_at"
t.datetime "updated_at"
t.index ["created_at", "author_id"], name: "analytics_index_audit_events_on_created_at_and_author_id"
- t.index ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type"
+ t.index ["entity_id", "entity_type", "id"], name: "index_audit_events_on_entity_id_and_entity_type_and_id_desc", order: { id: :desc }
end
create_table "award_emoji", id: :serial, force: :cascade do |t|
@@ -1979,6 +1979,15 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.index ["user_id"], name: "index_group_deletion_schedules_on_user_id"
end
+ create_table "group_deploy_tokens", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "group_id", null: false
+ t.bigint "deploy_token_id", null: false
+ t.index ["deploy_token_id"], name: "index_group_deploy_tokens_on_deploy_token_id"
+ t.index ["group_id", "deploy_token_id"], name: "index_group_deploy_tokens_on_group_and_deploy_token_ids", unique: true
+ end
+
create_table "group_group_links", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -3735,6 +3744,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.string "sso_url", null: false
t.boolean "enforced_sso", default: false, null: false
t.boolean "enforced_group_managed_accounts", default: false, null: false
+ t.boolean "prohibited_outer_forks", default: false, null: false
t.index ["group_id"], name: "index_saml_providers_on_group_id"
end
@@ -4133,6 +4143,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
t.boolean "sourcegraph_enabled"
t.boolean "setup_for_company"
t.boolean "render_whitespace_in_code"
+ t.integer "tab_width", limit: 2
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end
@@ -4691,6 +4702,8 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "group_deletion_schedules", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "group_deletion_schedules", "users", name: "fk_11e3ebfcdd", on_delete: :cascade
+ add_foreign_key "group_deploy_tokens", "deploy_tokens", on_delete: :cascade
+ add_foreign_key "group_deploy_tokens", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "group_group_links", "namespaces", column: "shared_group_id", on_delete: :cascade
add_foreign_key "group_group_links", "namespaces", column: "shared_with_group_id", on_delete: :cascade
add_foreign_key "identities", "saml_providers", name: "fk_aade90f0fc", on_delete: :cascade
diff --git a/doc/api/groups.md b/doc/api/groups.md
index ea2493111df..25a61632bd3 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -487,7 +487,7 @@ Parameters:
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (Maintainers), or `developer` (Developers + Maintainers). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
-| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
+| `subgroup_creation_level` | string | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
@@ -533,7 +533,7 @@ PUT /groups/:id
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (Maintainers), or `developer` (Developers + Maintainers). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
-| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
+| `subgroup_creation_level` | string | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 6b1a0e4ffe6..7e67ddc9021 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1970,7 +1970,7 @@ job:
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`expire_in` allows you to specify how long artifacts should live before they
-expire and therefore deleted, counting from the time they are uploaded and
+expire and are therefore deleted, counting from the time they are uploaded and
stored on GitLab. If the expiry time is not defined, it defaults to the
[instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only)
(30 days by default, forever on GitLab.com).
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4268e386425..cc9ef3ab5c5 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -762,6 +762,39 @@ networkPolicy:
app.gitlab.com/managed_by: gitlab
```
+#### Web Application Firewall (ModSecurity) customization
+
+> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/44) in GitLab 12.8.
+
+Customization on an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) or on a deployment base is available for clusters with [ModSecurity installed](../../user/clusters/applications.md#web-application-firewall-modsecurity).
+
+To enable ModSecurity with Auto Deploy, you need to create a `.gitlab/auto-deploy-values.yaml` file in your project with the following attributes.
+
+|Attribute | Description | Default |
+-----------|-------------|---------|
+|`enabled` | Enables custom configuration for modsecurity, defaulting to the [Core Rule Set](https://coreruleset.org/) | `false` |
+|`secRuleEngine` | Configures the [rules engine](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#secruleengine) | `DetectionOnly` |
+|`secRules` | Creates one or more additional [rule](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRule) | `nil` |
+
+In the following `auto-deploy-values.yaml` example, some custom settings
+are enabled for ModSecurity. Those include setting its engine to
+process rules instead of only logging them, while adding two specific
+rules which are header-based:
+
+```yaml
+ingress:
+ modSecurity:
+ enabled: true
+ secRuleEngine: "On"
+ secRules:
+ - variable: "REQUEST_HEADERS:User-Agent"
+ operator: "printer"
+ action: "log,deny,id:'2010',status:403,msg:'printer is an invalid agent'"
+ - variable: "REQUEST_HEADERS:Content-Type"
+ operator: "text/plain"
+ action: "log,deny,id:'2011',status:403,msg:'Text is not supported as content type'"
+```
+
#### Running commands in the container
Applications built with [Auto Build](#auto-build) using Herokuish, the default
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
index 2828d487153..992f4137bb8 100644
--- a/doc/user/application_security/dependency_list/index.md
+++ b/doc/user/application_security/dependency_list/index.md
@@ -5,7 +5,7 @@
The Dependency list allows you to see your project's dependencies, and key
details about them, including their known vulnerabilities. To see it,
navigate to **Security & Compliance > Dependency List** in your project's
-sidebar.
+sidebar. This information is sometimes referred to as a Software Bill of Materials or SBoM / BOM.
## Requirements
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index ea9c0b85bea..fad6d33dc7f 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -454,6 +454,12 @@ CI/CD configuration file to turn it on. Results are available in the SAST report
GitLab currently includes [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) checks.
+NOTE: **Note:**
+The secrets analyzer will ignore "Password in URL" vulnerabilities if the password begins
+with a dollar sign (`$`) as this likely indicates the password being used is an environment
+variable. For example, `https://username:$password@example.com/path/to/repo` will not be
+detected, whereas `https://username:password@example.com/path/to/repo` would be detected.
+
## Security Dashboard
The Security Dashboard is a good place to get an overview of all the security
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index b299c74c8f4..cd195e6e7a1 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -108,6 +108,15 @@ You can choose between 3 options:
- Readme
- Activity
+### Tab width
+
+You can set the displayed width of tab characters across various parts of
+GitLab, for example, blobs, diffs, and snippets.
+
+NOTE: **Note:**
+Some parts of GitLab do not respect this setting, including the WebIDE, file
+editor and Markdown editor.
+
## Localization
### Language
diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md
index 685fdefe0c6..e87b5d03438 100644
--- a/doc/user/project/operations/error_tracking.md
+++ b/doc/user/project/operations/error_tracking.md
@@ -25,7 +25,7 @@ GitLab provides an easy way to connect Sentry to your project:
Make sure to give the token at least the following scopes: `event:read` and `project:read`.
1. Navigate to your project’s **Settings > Operations**.
1. Ensure that the **Active** checkbox is set.
-1. In the **Sentry API URL** field, enter your Sentry hostname. For example, `https://sentry.example.com`.
+1. In the **Sentry API URL** field, enter your Sentry hostname. For example, enter `https://sentry.example.com` if this is the address at which your Sentry instance is available. For the SaaS version of Sentry, the hostname will be `https://sentry.io`.
1. In the **Auth Token** field, enter the token you previously generated.
1. Click the **Connect** button to test the connection to Sentry and populate the **Project** dropdown.
1. From the **Project** dropdown, choose a Sentry project to link to your GitLab project.
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 821c68dbedc..1329357d0b8 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -49,7 +49,7 @@ module Gitlab
lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
- deploy_token_check(login, password) ||
+ deploy_token_check(login, password, project) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
@@ -208,7 +208,7 @@ module Gitlab
end.uniq
end
- def deploy_token_check(login, password)
+ def deploy_token_check(login, password, project)
return unless password.present?
token = DeployToken.active.find_by_token(password)
@@ -219,7 +219,7 @@ module Gitlab
scopes = abilities_for_scopes(token.scopes)
if valid_scoped_token?(token, all_available_scopes)
- Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes)
+ Gitlab::Auth::Result.new(token, project, :deploy_token, scopes)
end
end
diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb
index e48041d9218..61c9c984f8e 100644
--- a/lib/gitlab/email/hook/smime_signature_interceptor.rb
+++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb
@@ -11,6 +11,7 @@ module Gitlab
cert: certificate.cert,
key: certificate.key,
data: message.encoded)
+
signed_email = Mail.new(signed_message)
overwrite_body(message, signed_email)
diff --git a/lib/gitlab/tab_width.rb b/lib/gitlab/tab_width.rb
new file mode 100644
index 00000000000..d33723a2106
--- /dev/null
+++ b/lib/gitlab/tab_width.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module TabWidth
+ extend self
+
+ MIN = 1
+ MAX = 12
+ DEFAULT = 8
+
+ def css_class_for_user(user)
+ return css_class_for_value(DEFAULT) unless user
+
+ css_class_for_value(user.tab_width)
+ end
+
+ private
+
+ def css_class_for_value(value)
+ raise ArgumentError unless in_range?(value)
+
+ "tab-width-#{value}"
+ end
+
+ def in_range?(value)
+ (MIN..MAX).cover?(value)
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9062f1fc5ac..23978006af0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -484,7 +484,7 @@ msgstr ""
msgid "%{username}'s avatar"
msgstr ""
-msgid "%{value} ms"
+msgid "%{value} s"
msgstr ""
msgid "%{verb} %{time_spent_value} spent time."
@@ -1670,6 +1670,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred loading code navigation"
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -9297,6 +9300,9 @@ msgstr ""
msgid "Go to commits"
msgstr ""
+msgid "Go to definition"
+msgstr ""
+
msgid "Go to environments"
msgstr ""
@@ -13963,6 +13969,9 @@ msgstr ""
msgid "Preferences|Layout width"
msgstr ""
+msgid "Preferences|Must be a number between %{min} and %{max}"
+msgstr ""
+
msgid "Preferences|Navigation theme"
msgstr ""
@@ -13981,6 +13990,9 @@ msgstr ""
msgid "Preferences|Syntax highlighting theme"
msgstr ""
+msgid "Preferences|Tab width"
+msgstr ""
+
msgid "Preferences|These settings will update how dates and times are displayed for you."
msgstr ""
@@ -19227,6 +19239,9 @@ msgstr ""
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
msgstr ""
+msgid "This Project is currently archived and read-only. Please unarchive the project first if you want to resume Pull mirroring"
+msgstr ""
+
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index 3e000c6381e..887625c4aa8 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -4,6 +4,7 @@ module QA
context 'Verify', :docker do
describe 'Pipeline creation and processing' do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
+ let(:max_wait) { 30 }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
@@ -68,11 +69,11 @@ module QA
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).to be_running(wait: 30)
- expect(pipeline).to have_build('test-success', status: :success)
- expect(pipeline).to have_build('test-failure', status: :failed)
- expect(pipeline).to have_build('test-tags', status: :pending)
- expect(pipeline).to have_build('test-artifacts', status: :success)
+ expect(pipeline).to be_running(wait: max_wait)
+ expect(pipeline).to have_build('test-success', status: :success, wait: max_wait)
+ expect(pipeline).to have_build('test-failure', status: :failed, wait: max_wait)
+ expect(pipeline).to have_build('test-tags', status: :pending, wait: max_wait)
+ expect(pipeline).to have_build('test-artifacts', status: :success, wait: max_wait)
end
end
end
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 77e7b32af25..98a9c3eaec6 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -47,6 +47,7 @@ describe Profiles::PreferencesController do
theme_id: '2',
first_day_of_week: '1',
preferred_language: 'jp',
+ tab_width: '5',
render_whitespace_in_code: 'true'
}.with_indifferent_access
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index 42ed66ac191..e86d4ab8812 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -9,6 +9,7 @@ FactoryBot.define do
read_registry { true }
revoked { false }
expires_at { 5.days.from_now }
+ deploy_token_type { DeployToken.deploy_token_types[:project_type] }
trait :revoked do
revoked { true }
@@ -21,5 +22,13 @@ FactoryBot.define do
trait :expired do
expires_at { Date.today - 1.month }
end
+
+ trait :group do
+ deploy_token_type { DeployToken.deploy_token_types[:group_type] }
+ end
+
+ trait :project do
+ deploy_token_type { DeployToken.deploy_token_types[:project_type] }
+ end
end
end
diff --git a/spec/factories/group_deploy_tokens.rb b/spec/factories/group_deploy_tokens.rb
new file mode 100644
index 00000000000..9ec7d0701be
--- /dev/null
+++ b/spec/factories/group_deploy_tokens.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :group_deploy_token do
+ group
+ deploy_token
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index f83c137b758..34f6da682b6 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -23,6 +23,10 @@ FactoryBot.define do
after(:build) { |user, _| user.block! }
end
+ trait :bot do
+ bot_type { User.bot_types[:alert_bot] }
+ end
+
trait :external do
external { true }
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 5662465d431..8c16dcec42f 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -3,53 +3,53 @@
require 'spec_helper'
describe 'Group navbar' do
- it_behaves_like 'verified navigation bar' do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ let(:analytics_nav_item) do
+ {
+ nav_item: _('Analytics'),
+ nav_sub_items: [
+ _('Contribution Analytics')
+ ]
+ }
+ end
- let(:analytics_nav_item) do
+ let(:structure) do
+ [
+ {
+ nav_item: _('Group overview'),
+ nav_sub_items: [
+ _('Details'),
+ _('Activity')
+ ]
+ },
{
- nav_item: _('Analytics'),
+ nav_item: _('Issues'),
nav_sub_items: [
- _('Contribution Analytics')
+ _('List'),
+ _('Board'),
+ _('Labels'),
+ _('Milestones')
]
+ },
+ {
+ nav_item: _('Merge Requests'),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _('Kubernetes'),
+ nav_sub_items: []
+ },
+ (analytics_nav_item if Gitlab.ee?),
+ {
+ nav_item: _('Members'),
+ nav_sub_items: []
}
- end
-
- let(:structure) do
- [
- {
- nav_item: _('Group overview'),
- nav_sub_items: [
- _('Details'),
- _('Activity')
- ]
- },
- {
- nav_item: _('Issues'),
- nav_sub_items: [
- _('List'),
- _('Board'),
- _('Labels'),
- _('Milestones')
- ]
- },
- {
- nav_item: _('Merge Requests'),
- nav_sub_items: []
- },
- {
- nav_item: _('Kubernetes'),
- nav_sub_items: []
- },
- (analytics_nav_item if Gitlab.ee?),
- {
- nav_item: _('Members'),
- nav_sub_items: []
- }
- ]
- end
+ ]
+ end
+ it_behaves_like 'verified navigation bar' do
before do
group.add_maintainer(user)
sign_in(user)
@@ -57,4 +57,21 @@ describe 'Group navbar' do
visit group_path(group)
end
end
+
+ if Gitlab.ee?
+ context 'when productivity analytics is available' do
+ before do
+ stub_licensed_features(productivity_analytics: true)
+
+ analytics_nav_item[:nav_sub_items] << _('Productivity Analytics')
+
+ group.add_maintainer(user)
+ sign_in(user)
+
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
+ end
end
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 4f2c5fc73d8..17ff494a6fa 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -20,7 +20,7 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
end
before do
- stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
+ stub_feature_flags(web_ide_default: false, single_mr_diff_view: false, code_navigation: false)
target_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb
index 2d2da222998..6e61536d5ff 100644
--- a/spec/features/profiles/user_edit_preferences_spec.rb
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -29,4 +29,31 @@ describe 'User edit preferences profile' do
expect(field).not_to be_checked
end
+
+ describe 'User changes tab width to acceptable value' do
+ it 'shows success message' do
+ fill_in 'Tab width', with: 9
+ click_button 'Save changes'
+
+ expect(page).to have_content('Preferences saved.')
+ end
+
+ it 'saves the value' do
+ tab_width_field = page.find_field('Tab width')
+
+ expect do
+ tab_width_field.fill_in with: 6
+ click_button 'Save changes'
+ end.to change { tab_width_field.value }
+ end
+ end
+
+ describe 'User changes tab width to unacceptable value' do
+ it 'shows error message' do
+ fill_in 'Tab width', with: -1
+ click_button 'Save changes'
+
+ expect(page).to have_content('Failed to save preferences')
+ end
+ end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 5d86e4125df..e714d0f7cad 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -13,6 +13,10 @@ describe 'File blob', :js do
wait_for_requests
end
+ before do
+ stub_feature_flags(code_navigation: false)
+ end
+
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index a1d6a8896c7..5d62b2f87bb 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -69,6 +69,8 @@ describe 'Editing file blob', :js do
context 'from blob file path' do
before do
+ stub_feature_flags(code_navigation: false)
+
visit project_blob_path(project, tree_join(branch, file_path))
end
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
index b90129d6176..30878b7fb64 100644
--- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -8,6 +8,7 @@ describe 'User creates blob in new project', :js do
shared_examples 'creating a file' do
before do
+ stub_feature_flags(code_navigation: false)
sign_in(user)
visit project_path(project)
end
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index eb9a4d8cb09..2d4f22e299e 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -14,7 +14,7 @@ describe 'Projects > Files > User creates files', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(web_ide_default: false)
+ stub_feature_flags(web_ide_default: false, code_navigation: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index 0f543e47631..5e36407d9cb 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -14,6 +14,8 @@ describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(code_navigation: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 4c54bbdcd67..e1eefdcc40f 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -16,6 +16,8 @@ describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(code_navigation: false)
+
sign_in(user)
end
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
new file mode 100644
index 00000000000..dda6d68018e
--- /dev/null
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Code navigation popover component renders popover 1`] = `
+<div
+ class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
+ style="left: 0px; top: 0px;"
+>
+ <div
+ class="arrow"
+ style="left: 0px;"
+ />
+
+ <div
+ class="border-bottom"
+ >
+ <pre
+ class="border-0 bg-transparent m-0 code highlight"
+ >
+ console.log
+ </pre>
+ </div>
+
+ <div
+ class="popover-body"
+ >
+ <gl-button-stub
+ class="w-100"
+ href="http://test.com"
+ size="md"
+ target="_blank"
+ variant="default"
+ >
+
+ Go to definition
+
+ </gl-button-stub>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
new file mode 100644
index 00000000000..cfdc0dcc6cc
--- /dev/null
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -0,0 +1,64 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import createState from '~/code_navigation/store/state';
+import App from '~/code_navigation/components/app.vue';
+import Popover from '~/code_navigation/components/popover.vue';
+
+const localVue = createLocalVue();
+const fetchData = jest.fn();
+const showDefinition = jest.fn();
+let wrapper;
+
+localVue.use(Vuex);
+
+function factory(initialState = {}) {
+ const store = new Vuex.Store({
+ state: {
+ ...createState(),
+ ...initialState,
+ },
+ actions: {
+ fetchData,
+ showDefinition,
+ },
+ });
+
+ wrapper = shallowMount(App, { store, localVue });
+}
+
+describe('Code navigation app component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('fetches data on mount', () => {
+ factory();
+
+ expect(fetchData).toHaveBeenCalled();
+ });
+
+ it('hides popover when no definition set', () => {
+ factory();
+
+ expect(wrapper.find(Popover).exists()).toBe(false);
+ });
+
+ it('renders popover when definition set', () => {
+ factory({
+ currentDefinition: { hover: 'console' },
+ currentDefinitionPosition: { x: 0 },
+ });
+
+ expect(wrapper.find(Popover).exists()).toBe(true);
+ });
+
+ it('calls showDefinition when clicking blob viewer', () => {
+ setFixtures('<div class="blob-viewer"></div>');
+
+ factory();
+
+ document.querySelector('.blob-viewer').click();
+
+ expect(showDefinition).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
new file mode 100644
index 00000000000..ad05504a224
--- /dev/null
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -0,0 +1,58 @@
+import { shallowMount } from '@vue/test-utils';
+import Popover from '~/code_navigation/components/popover.vue';
+
+const MOCK_CODE_DATA = Object.freeze({
+ hover: [
+ {
+ language: 'javascript',
+ value: 'console.log',
+ },
+ ],
+ definition_url: 'http://test.com',
+});
+
+const MOCK_DOCS_DATA = Object.freeze({
+ hover: [
+ {
+ language: null,
+ value: 'console.log',
+ },
+ ],
+ definition_url: 'http://test.com',
+});
+
+let wrapper;
+
+function factory(position, data) {
+ wrapper = shallowMount(Popover, { propsData: { position, data } });
+}
+
+describe('Code navigation popover component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders popover', () => {
+ factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('code output', () => {
+ it('renders code output', () => {
+ factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
+
+ expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
+ expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
+ });
+ });
+
+ describe('documentation output', () => {
+ it('renders code output', () => {
+ factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA);
+
+ expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
+ expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
new file mode 100644
index 00000000000..5e29a76f804
--- /dev/null
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -0,0 +1,221 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import actions from '~/code_navigation/store/actions';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
+
+jest.mock('~/flash');
+jest.mock('~/code_navigation/utils');
+
+describe('Code navigation actions', () => {
+ describe('setInitialData', () => {
+ it('commits SET_INITIAL_DATA', done => {
+ testAction(
+ actions.setInitialData,
+ { projectPath: 'test' },
+ {},
+ [{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test' } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestDataError', () => {
+ it('commits REQUEST_DATA_ERROR', () =>
+ testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []));
+
+ it('creates a flash message', () =>
+ testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then(
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ },
+ ));
+ });
+
+ describe('fetchData', () => {
+ let mock;
+ const state = {
+ projectPath: 'gitlab-org/gitlab',
+ commitId: '123',
+ blobPath: 'index',
+ };
+ const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
+
+ beforeEach(() => {
+ window.gon = { api_version: '1' };
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(apiUrl).replyOnce(200, [
+ {
+ start_line: 0,
+ start_char: 0,
+ hover: { value: '123' },
+ },
+ {
+ start_line: 1,
+ start_char: 0,
+ hover: null,
+ },
+ ]);
+ });
+
+ it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
+ testAction(
+ actions.fetchData,
+ null,
+ state,
+ [
+ { type: 'REQUEST_DATA' },
+ {
+ type: 'REQUEST_DATA_SUCCESS',
+ payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('calls addInteractionClass with data', done => {
+ testAction(
+ actions.fetchData,
+ null,
+ state,
+ [
+ { type: 'REQUEST_DATA' },
+ {
+ type: 'REQUEST_DATA_SUCCESS',
+ payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } },
+ },
+ ],
+ [],
+ )
+ .then(() => {
+ expect(addInteractionClass).toHaveBeenCalledWith({
+ start_line: 0,
+ start_char: 0,
+ hover: { value: '123' },
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(apiUrl).replyOnce(500);
+ });
+
+ it('dispatches requestDataError', done => {
+ testAction(
+ actions.fetchData,
+ null,
+ state,
+ [{ type: 'REQUEST_DATA' }],
+ [{ type: 'requestDataError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('showDefinition', () => {
+ let target;
+
+ beforeEach(() => {
+ target = document.createElement('div');
+ });
+
+ it('returns early when no data exists', done => {
+ testAction(actions.showDefinition, { target }, {}, [], [], done);
+ });
+
+ it('commits SET_CURRENT_DEFINITION when target is not code navitation element', done => {
+ testAction(
+ actions.showDefinition,
+ { target },
+ { data: {} },
+ [
+ {
+ type: 'SET_CURRENT_DEFINITION',
+ payload: { definition: undefined, position: undefined },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('commits SET_CURRENT_DEFINITION with LSIF data', done => {
+ target.classList.add('js-code-navigation');
+ target.setAttribute('data-line-index', '0');
+ target.setAttribute('data-char-index', '0');
+
+ testAction(
+ actions.showDefinition,
+ { target },
+ { data: { '0:0': { hover: 'test' } } },
+ [
+ {
+ type: 'SET_CURRENT_DEFINITION',
+ payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('adds hll class to target element', () => {
+ target.classList.add('js-code-navigation');
+ target.setAttribute('data-line-index', '0');
+ target.setAttribute('data-char-index', '0');
+
+ return testAction(
+ actions.showDefinition,
+ { target },
+ { data: { '0:0': { hover: 'test' } } },
+ [
+ {
+ type: 'SET_CURRENT_DEFINITION',
+ payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ },
+ ],
+ [],
+ ).then(() => {
+ expect(target.classList).toContain('hll');
+ });
+ });
+
+ it('caches current target element', () => {
+ target.classList.add('js-code-navigation');
+ target.setAttribute('data-line-index', '0');
+ target.setAttribute('data-char-index', '0');
+
+ return testAction(
+ actions.showDefinition,
+ { target },
+ { data: { '0:0': { hover: 'test' } } },
+ [
+ {
+ type: 'SET_CURRENT_DEFINITION',
+ payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ },
+ ],
+ [],
+ ).then(() => {
+ expect(setCurrentHoverElement).toHaveBeenCalledWith(target);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js
new file mode 100644
index 00000000000..117a2ed2f14
--- /dev/null
+++ b/spec/frontend/code_navigation/store/mutations_spec.js
@@ -0,0 +1,63 @@
+import mutations from '~/code_navigation/store/mutations';
+import createState from '~/code_navigation/store/state';
+
+let state;
+
+describe('Code navigation mutations', () => {
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets initial data', () => {
+ mutations.SET_INITIAL_DATA(state, {
+ projectPath: 'test',
+ commitId: '123',
+ blobPath: 'index.js',
+ });
+
+ expect(state.projectPath).toBe('test');
+ expect(state.commitId).toBe('123');
+ expect(state.blobPath).toBe('index.js');
+ });
+ });
+
+ describe('REQUEST_DATA', () => {
+ it('sets loading true', () => {
+ mutations.REQUEST_DATA(state);
+
+ expect(state.loading).toBe(true);
+ });
+ });
+
+ describe('REQUEST_DATA_SUCCESS', () => {
+ it('sets loading false', () => {
+ mutations.REQUEST_DATA_SUCCESS(state, ['test']);
+
+ expect(state.loading).toBe(false);
+ });
+
+ it('sets data', () => {
+ mutations.REQUEST_DATA_SUCCESS(state, ['test']);
+
+ expect(state.data).toEqual(['test']);
+ });
+ });
+
+ describe('REQUEST_DATA_ERROR', () => {
+ it('sets loading false', () => {
+ mutations.REQUEST_DATA_ERROR(state);
+
+ expect(state.loading).toBe(false);
+ });
+ });
+
+ describe('SET_CURRENT_DEFINITION', () => {
+ it('sets current definition and position', () => {
+ mutations.SET_CURRENT_DEFINITION(state, { definition: 'test', position: { x: 0 } });
+
+ expect(state.currentDefinition).toBe('test');
+ expect(state.currentDefinitionPosition).toEqual({ x: 0 });
+ });
+ });
+});
diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js
new file mode 100644
index 00000000000..458cc536635
--- /dev/null
+++ b/spec/frontend/code_navigation/utils/index_spec.js
@@ -0,0 +1,58 @@
+import {
+ cachedData,
+ getCurrentHoverElement,
+ setCurrentHoverElement,
+ addInteractionClass,
+} from '~/code_navigation/utils';
+
+afterEach(() => {
+ if (cachedData.has('current')) {
+ cachedData.delete('current');
+ }
+});
+
+describe('getCurrentHoverElement', () => {
+ it.each`
+ value
+ ${'test'}
+ ${undefined}
+ `('it returns cached current key', ({ value }) => {
+ if (value) {
+ cachedData.set('current', value);
+ }
+
+ expect(getCurrentHoverElement()).toEqual(value);
+ });
+});
+
+describe('setCurrentHoverElement', () => {
+ it('sets cached current key', () => {
+ setCurrentHoverElement('test');
+
+ expect(getCurrentHoverElement()).toEqual('test');
+ });
+});
+
+describe('addInteractionClass', () => {
+ beforeEach(() => {
+ setFixtures(
+ '<div id="LC1"><span>console</span><span>.</span><span>log</span></div><div id="LC2"><span>function</span></div>',
+ );
+ });
+
+ it.each`
+ line | char | index
+ ${0} | ${0} | ${0}
+ ${0} | ${8} | ${2}
+ ${1} | ${0} | ${0}
+ `(
+ 'it sets code navigation attributes for line $line and character $char',
+ ({ line, char, index }) => {
+ addInteractionClass({ start_line: line, start_char: char });
+
+ expect(document.querySelectorAll(`#LC${line + 1} span`)[index].classList).toContain(
+ 'js-code-navigation',
+ );
+ },
+ );
+});
diff --git a/spec/initializers/mail_encoding_patch_spec.rb b/spec/initializers/mail_encoding_patch_spec.rb
new file mode 100644
index 00000000000..41074af3503
--- /dev/null
+++ b/spec/initializers/mail_encoding_patch_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require 'mail'
+require_relative '../../config/initializers/mail_encoding_patch.rb'
+
+describe 'Mail quoted-printable transfer encoding patch and Unicode characters' do
+ shared_examples 'email encoding' do |email|
+ it 'enclosing in a new object does not change the encoded original' do
+ new_email = Mail.new(email)
+
+ expect(new_email.subject).to eq(email.subject)
+ expect(new_email.from).to eq(email.from)
+ expect(new_email.to).to eq(email.to)
+ expect(new_email.content_type).to eq(email.content_type)
+ expect(new_email.content_transfer_encoding).to eq(email.content_transfer_encoding)
+
+ expect(new_email.encoded).to eq(email.encoded)
+ end
+ end
+
+ context 'with a text email' do
+ context 'with a body that encodes to exactly 74 characters (final newline)' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/plain; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-1\n"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+
+ context 'with a body that encodes to exactly 74 characters (no final newline)' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/plain; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+
+ context 'with a body that encodes to exactly 75 characters' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/plain; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12\n"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+ end
+
+ context 'with an html email' do
+ context 'with a body that encodes to exactly 74 characters (final newline)' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/html; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-1234</p>\n"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+
+ context 'with a body that encodes to exactly 74 characters (no final newline)' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/html; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+
+ context 'with a body that encodes to exactly 75 characters' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/html; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>\n"
+ end
+
+ it_behaves_like 'email encoding', email
+ end
+ end
+
+ context 'a multipart email' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ end
+
+ text_part = Mail::Part.new do
+ content_type 'text/plain; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n"
+ end
+
+ html_part = Mail::Part.new do
+ content_type 'text/html; charset=UTF-8'
+ content_transfer_encoding 'quoted-printable'
+ body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n"
+ end
+
+ email.text_part = text_part
+ email.html_part = html_part
+
+ it_behaves_like 'email encoding', email
+ end
+
+ context 'with non UTF-8 charset' do
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ content_type 'text/plain; charset=windows-1251'
+ content_transfer_encoding 'quoted-printable'
+ body "This line is very long and will be put in multiple quoted-printable lines. Some Russian character: Д\n\n\n".encode('windows-1251')
+ end
+
+ it_behaves_like 'email encoding', email
+
+ it 'can be decoded back' do
+ expect(Mail.new(email).body.decoded.dup.force_encoding('windows-1251').encode('utf-8')).to include('Some Russian character: Д')
+ end
+ end
+
+ context 'with binary content' do
+ context 'can be encoded with \'base64\' content-transfer-encoding' do
+ image = File.binread('spec/fixtures/rails_sample.jpg')
+
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ end
+
+ part = Mail::Part.new
+ part.body = [image].pack('m')
+ part.content_type = 'image/jpg'
+ part.content_transfer_encoding = 'base64'
+
+ email.parts << part
+
+ it_behaves_like 'email encoding', email
+
+ it 'binary contents are not modified' do
+ expect(email.parts.first.decoded).to eq(image)
+
+ # Enclosing in a new Mail object does not corrupt encoded data
+ expect(Mail.new(email).parts.first.decoded).to eq(image)
+ end
+ end
+
+ context 'encoding fails with \'quoted-printable\' content-transfer-encoding' do
+ image = File.binread('spec/fixtures/rails_sample.jpg')
+
+ email = Mail.new do
+ to 'jane.doe@example.com'
+ from 'John Dóe <john.doe@example.com>'
+ subject 'Encoding tést'
+ end
+
+ part = Mail::Part.new
+ part.body = [image].pack('M*')
+ part.content_type = 'image/jpg'
+ part.content_transfer_encoding = 'quoted-printable'
+
+ email.parts << part
+
+ # The Mail patch in `config/initializers/mail_encoding_patch.rb` fixes
+ # encoding of non-binary content. The failure below is expected since we
+ # reverted some upstream changes in order to properly support SMIME signatures
+ # See https://gitlab.com/gitlab-org/gitlab/issues/197386
+ it 'content cannot be decoded back' do
+ # Headers are ok
+ expect(email.subject).to eq(email.subject)
+ expect(email.from).to eq(email.from)
+ expect(email.to).to eq(email.to)
+ expect(email.content_type).to eq(email.content_type)
+ expect(email.content_transfer_encoding).to eq(email.content_transfer_encoding)
+
+ # Content cannot be recovered
+ expect(email.parts.first.decoded).not_to eq(image)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js
index d42c509e5b5..ff046e64b6e 100644
--- a/spec/javascripts/reports/components/modal_spec.js
+++ b/spec/javascripts/reports/components/modal_spec.js
@@ -42,8 +42,8 @@ describe('Grouped Test Reports Modal', () => {
);
});
- it('renders miliseconds', () => {
- expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} ms`);
+ it('renders seconds', () => {
+ expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} s`);
});
it('render title', () => {
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
index 3962f837a00..b3def474957 100644
--- a/spec/javascripts/user_popovers_spec.js
+++ b/spec/javascripts/user_popovers_spec.js
@@ -38,6 +38,13 @@ describe('User Popovers', () => {
expect(document.querySelectorAll(selector).length).toBe(popovers.length);
});
+ it('does not initialize the user popovers twice for the same element', () => {
+ const newPopovers = initUserPopovers(document.querySelectorAll(selector));
+ const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover);
+
+ expect(samePopovers).toBe(true);
+ });
+
describe('when user link emits mouseenter event', () => {
let userLink;
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 1f943bebbec..ed763f63756 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -460,6 +460,20 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
end
+ context 'when the deploy token is of group type' do
+ let(:project_with_group) { create(:project, group: create(:group)) }
+ let(:deploy_token) { create(:deploy_token, :group, read_repository: true, groups: [project_with_group.group]) }
+ let(:login) { deploy_token.username }
+
+ subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') }
+
+ it 'succeeds when login and a group deploy token are valid' do
+ auth_success = Gitlab::Auth::Result.new(deploy_token, project_with_group, :deploy_token, [:download_code, :read_container_image])
+
+ expect(subject).to eq(auth_success)
+ end
+ end
+
context 'when the deploy token has read_registry as a scope' do
let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) }
let(:login) { deploy_token.username }
@@ -469,10 +483,10 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
stub_container_registry_config(enabled: true)
end
- it 'succeeds when login and token are valid' do
+ it 'succeeds when login and a project token are valid' do
auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image])
- expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
+ expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
.to eq(auth_success)
end
diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
index a65214fab61..36954252b6b 100644
--- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -20,8 +20,14 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert])
end
+ let(:mail_body) { "signed hello with Unicode €áø and\r\n newlines\r\n" }
+
let(:mail) do
- ActionMailer::Base.mail(to: 'test@example.com', from: 'info@example.com', body: 'signed hello')
+ ActionMailer::Base.mail(to: 'test@example.com',
+ from: 'info@example.com',
+ content_transfer_encoding: 'quoted-printable',
+ content_type: 'text/plain; charset=UTF-8',
+ body: mail_body)
end
before do
@@ -46,9 +52,16 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
ca_cert: root_certificate.cert,
signed_data: mail.encoded)
+ # re-verify signature from a new Mail object content
+ # See https://gitlab.com/gitlab-org/gitlab/issues/197386
+ Gitlab::Email::Smime::Signer.verify_signature(
+ cert: certificate.cert,
+ ca_cert: root_certificate.cert,
+ signed_data: Mail.new(mail).encoded)
+
# envelope in a Mail object and obtain the body
decoded_mail = Mail.new(p7enc.data)
- expect(decoded_mail.body.encoded).to eq('signed hello')
+ expect(decoded_mail.body.decoded.dup.force_encoding(decoded_mail.charset)).to eq(mail_body)
end
end
diff --git a/spec/lib/gitlab/tab_width_spec.rb b/spec/lib/gitlab/tab_width_spec.rb
new file mode 100644
index 00000000000..3b5014d27e4
--- /dev/null
+++ b/spec/lib/gitlab/tab_width_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::TabWidth, lib: true do
+ describe '.css_class_for_user' do
+ it 'returns default CSS class when user is nil' do
+ css_class = described_class.css_class_for_user(nil)
+
+ expect(css_class).to eq('tab-width-8')
+ end
+
+ it "returns CSS class for user's tab width", :aggregate_failures do
+ [1, 6, 12].each do |i|
+ user = double('user', tab_width: i)
+ css_class = described_class.css_class_for_user(user)
+
+ expect(css_class).to eq("tab-width-#{i}")
+ end
+ end
+
+ it 'raises if tab width is out of valid range', :aggregate_failures do
+ [0, 13, 'foo', nil].each do |i|
+ expect do
+ user = double('user', tab_width: i)
+ described_class.css_class_for_user(user)
+ end.to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 5c14d57cf18..568699cf3f6 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -7,6 +7,8 @@ describe DeployToken do
it { is_expected.to have_many :project_deploy_tokens }
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
+ it { is_expected.to have_many :group_deploy_tokens }
+ it { is_expected.to have_many(:groups).through(:group_deploy_tokens) }
it_behaves_like 'having unique enum values'
@@ -17,6 +19,29 @@ describe DeployToken do
it { is_expected.to allow_value('GitLab+deploy_token-3.14').for(:username) }
it { is_expected.not_to allow_value('<script>').for(:username).with_message(username_format_message) }
it { is_expected.not_to allow_value('').for(:username).with_message(username_format_message) }
+ it { is_expected.to validate_presence_of(:deploy_token_type) }
+ end
+
+ describe 'deploy_token_type validations' do
+ context 'when a deploy token is associated to a group' do
+ it 'does not allow setting a project to it' do
+ group_token = create(:deploy_token, :group)
+ group_token.projects << build(:project)
+
+ expect(group_token).not_to be_valid
+ expect(group_token.errors.full_messages).to include('Deploy token cannot have projects assigned')
+ end
+ end
+
+ context 'when a deploy token is associated to a project' do
+ it 'does not allow setting a group to it' do
+ project_token = create(:deploy_token)
+ project_token.groups << build(:group)
+
+ expect(project_token).not_to be_valid
+ expect(project_token.errors.full_messages).to include('Deploy token cannot have groups assigned')
+ end
+ end
end
describe '#ensure_token' do
@@ -125,33 +150,148 @@ describe DeployToken do
end
end
+ describe '#holder' do
+ subject { deploy_token.holder }
+
+ context 'when the token is of project type' do
+ it 'returns the relevant holder token' do
+ expect(subject).to eq(deploy_token.project_deploy_tokens.first)
+ end
+ end
+
+ context 'when the token is of group type' do
+ let(:group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token, :group) }
+
+ it 'returns the relevant holder token' do
+ expect(subject).to eq(deploy_token.group_deploy_tokens.first)
+ end
+ end
+ end
+
describe '#has_access_to?' do
let(:project) { create(:project) }
subject { deploy_token.has_access_to?(project) }
- context 'when deploy token is active and related to project' do
- let(:deploy_token) { create(:deploy_token, projects: [project]) }
+ context 'when a project is not passed in' do
+ let(:project) { nil }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_falsy }
end
- context 'when deploy token is active but not related to project' do
- let(:deploy_token) { create(:deploy_token) }
+ context 'when a project is passed in' do
+ context 'when deploy token is active and related to project' do
+ let(:deploy_token) { create(:deploy_token, projects: [project]) }
- it { is_expected.to be_falsy }
- end
+ it { is_expected.to be_truthy }
+ end
- context 'when deploy token is revoked and related to project' do
- let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
+ context 'when deploy token is active but not related to project' do
+ let(:deploy_token) { create(:deploy_token) }
- it { is_expected.to be_falsy }
- end
+ it { is_expected.to be_falsy }
+ end
- context 'when deploy token is revoked and not related to the project' do
- let(:deploy_token) { create(:deploy_token, :revoked) }
+ context 'when deploy token is revoked and related to project' do
+ let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
- it { is_expected.to be_falsy }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when deploy token is revoked and not related to the project' do
+ let(:deploy_token) { create(:deploy_token, :revoked) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'and when the token is of group type' do
+ let_it_be(:group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token, :group) }
+
+ before do
+ deploy_token.groups << group
+ end
+
+ context 'and the allow_group_deploy_token feature flag is turned off' do
+ it 'is false' do
+ stub_feature_flags(allow_group_deploy_token: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'and the allow_group_deploy_token feature flag is turned on' do
+ before do
+ stub_feature_flags(allow_group_deploy_token: true)
+ end
+
+ context 'and the passed-in project does not belong to any group' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'and the passed-in project belongs to the token group' do
+ it 'is true' do
+ group.projects << project
+
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'and the passed-in project belongs to a subgroup' do
+ let(:child_group) { create(:group, parent_id: group.id) }
+ let(:grandchild_group) { create(:group, parent_id: child_group.id) }
+
+ before do
+ grandchild_group.projects << project
+ end
+
+ context 'and the token group is an ancestor (grand-parent) of this group' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and the token group is not ancestor of this group' do
+ let(:child2_group) { create(:group, parent_id: group.id) }
+
+ it 'is false' do
+ deploy_token.groups = [child2_group]
+
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ context 'and the passed-in project does not belong to the token group' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'and the project belongs to a group that is parent of the token group' do
+ let(:super_group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token, :group) }
+ let(:group) { create(:group, parent_id: super_group.id) }
+
+ it 'is false' do
+ super_group.projects << project
+
+ is_expected.to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'and the token is of project type' do
+ let(:deploy_token) { create(:deploy_token, projects: [project]) }
+
+ context 'and the passed-in project is the same as the token project' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and the passed-in project is not the same as the token project' do
+ subject { deploy_token.has_access_to?(create(:project)) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
end
@@ -183,7 +323,7 @@ describe DeployToken do
end
end
- context 'when passign a value' do
+ context 'when passing a value' do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
diff --git a/spec/models/group_deploy_token_spec.rb b/spec/models/group_deploy_token_spec.rb
new file mode 100644
index 00000000000..d38abafa7ed
--- /dev/null
+++ b/spec/models/group_deploy_token_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupDeployToken, type: :model do
+ let(:group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token) }
+
+ subject(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) }
+
+ it { is_expected.to belong_to :group }
+ it { is_expected.to belong_to :deploy_token }
+
+ it { is_expected.to validate_presence_of :deploy_token }
+ it { is_expected.to validate_presence_of :group }
+ it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:group_id) }
+end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index bb88983e140..7884b87cc26 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -85,4 +85,19 @@ describe UserPreference do
expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
end
end
+
+ describe '#tab_width' do
+ it 'is set to 8 by default' do
+ # Intentionally not using factory here to test the constructor.
+ pref = UserPreference.new
+ expect(pref.tab_width).to eq(8)
+ end
+
+ it do
+ is_expected.to validate_numericality_of(:tab_width)
+ .only_integer
+ .is_greater_than_or_equal_to(1)
+ .is_less_than_or_equal_to(12)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 74e38e79616..855b8e3a8a7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -20,6 +20,9 @@ describe User, :do_not_mock_admin_mode do
describe 'delegations' do
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
+
+ it { is_expected.to delegate_method(:tab_width).to(:user_preference) }
+ it { is_expected.to delegate_method(:tab_width=).to(:user_preference).with_arguments(5) }
end
describe 'associations' do
@@ -4126,4 +4129,41 @@ describe User, :do_not_mock_admin_mode do
end
end
end
+
+ describe 'internal methods' do
+ let_it_be(:user) { create(:user) }
+ let!(:ghost) { described_class.ghost }
+ let!(:alert_bot) { described_class.alert_bot }
+ let!(:non_internal) { [user] }
+ let!(:internal) { [ghost, alert_bot] }
+
+ it 'returns non internal users' do
+ expect(described_class.internal).to eq(internal)
+ expect(internal.all?(&:internal?)).to eq(true)
+ end
+
+ it 'returns internal users' do
+ expect(described_class.non_internal).to eq(non_internal)
+ expect(non_internal.all?(&:internal?)).to eq(false)
+ end
+
+ describe '#bot?' do
+ it 'marks bot users' do
+ expect(user.bot?).to eq(false)
+ expect(ghost.bot?).to eq(false)
+
+ expect(alert_bot.bot?).to eq(true)
+ end
+ end
+ end
+
+ describe 'bots & humans' do
+ it 'returns corresponding users' do
+ human = create(:user)
+ bot = create(:user, :bot)
+
+ expect(described_class.humans).to match_array([human])
+ expect(described_class.bots).to match_array([bot])
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 1a4b8315fde..3b08726c75a 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -559,4 +559,18 @@ describe ProjectPolicy do
end
end
end
+
+ context 'alert bot' do
+ let(:current_user) { User.alert_bot }
+
+ subject { described_class.new(current_user, project) }
+
+ it { is_expected.to be_allowed(:reporter_access) }
+
+ context 'within a private project' do
+ let(:project) { create(:project, :private) }
+
+ it { is_expected.to be_allowed(:admin_issue) }
+ end
+ end
end