diff options
291 files changed, 3102 insertions, 522 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90bc9f74ef3..9c5b40dce21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -95,7 +95,7 @@ stages: # Skip all jobs except the ones that begin with 'docs/'. # Used for commits including ONLY documentation changes. -# https://docs.gitlab.com/ce/development/writing_documentation.html#testing +# https://docs.gitlab.com/ce/development/documentation/#testing .except-docs: &except-docs except: - /(^docs[\/-].*|.*-docs$)/ @@ -729,6 +729,9 @@ karma: dependencies: - compile-assets - setup-test-env + variables: + # we override the max_old_space_size to prevent OOM errors + NODE_OPTIONS: --max_old_space_size=3584 script: - export BABEL_ENV=coverage CHROME_LOG_FILE=chrome_debug.log - date diff --git a/.rubocop.yml b/.rubocop.yml index ce7be208186..b7aec5b8b14 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -35,6 +35,25 @@ Style/MutableConstant: Style/SafeNavigation: Enabled: false +# Frozen String Literal +Style/FrozenStringLiteralComment: + Enabled: true + Exclude: + - 'config.ru' + - 'Dangerfile' + - 'Gemfile' + - 'Rakefile' + - 'app/views/**/*' + - 'config/**/*' + - 'danger/**/*' + - 'db/**/*' + - 'ee/**/*' + - 'lib/**/*' + - 'qa/**/*' + - 'rubocop/**/*' + - 'scripts/**/*' + - 'spec/**/*' + Naming/FileName: ExpectMatchingDefinition: true Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index e514a42108c..c9ab8599d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.3.1 (2018-09-26) + +### Security (6 changes) + +- Redact confidential events in the API. +- Set timeout for syntax highlighting. +- Sanitize JSON data properly to fix XSS on Issue details page. +- Fix stored XSS in merge requests from imported repository. +- Fix xss vulnerability sourced from package.json. +- Block loopback addresses in UrlBlocker. + + ## 11.3.0 (2018-09-22) ### Security (5 changes, 1 of them is from the community) @@ -249,6 +261,18 @@ entry. - Creates Vue component for artifacts block on job page. +## 11.2.4 (2018-09-26) + +### Security (6 changes) + +- Redact confidential events in the API. +- Set timeout for syntax highlighting. +- Sanitize JSON data properly to fix XSS on Issue details page. +- Fix stored XSS in merge requests from imported repository. +- Fix xss vulnerability sourced from package.json. +- Block loopback addresses in UrlBlocker. + + ## 11.2.3 (2018-08-28) ### Fixed (1 change) @@ -516,6 +540,18 @@ entry. - Moves help_popover component to a common location. +## 11.1.7 (2018-09-26) + +### Security (6 changes) + +- Redact confidential events in the API. +- Set timeout for syntax highlighting. +- Sanitize JSON data properly to fix XSS on Issue details page. +- Fix stored XSS in merge requests from imported repository. +- Fix xss vulnerability sourced from package.json. +- Block loopback addresses in UrlBlocker. + + ## 11.1.6 (2018-08-28) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ee1e4d2aee5..3ba7bd5ba83 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.122.0 +0.123.0 @@ -112,9 +112,6 @@ gem 'hamlit', '~> 2.8.8' gem 'carrierwave', '= 1.2.3' gem 'mini_magick' -# Drag and Drop UI -gem 'dropzonejs-rails', '~> 0.7.1' - # for backups gem 'fog-aws', '~> 2.0.1' gem 'fog-core', '~> 1.44' @@ -423,7 +420,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly' gem 'grpc', '~> 1.11.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 877a07ea007..69a1c899cc5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,8 +175,6 @@ GEM doorkeeper-openid_connect (1.5.0) doorkeeper (~> 4.3) json-jwt (~> 1.6) - dropzonejs-rails (0.7.2) - rails (> 3.1) ed25519 (1.2.4) email_reply_trimmer (0.1.6) email_spec (2.2.0) @@ -276,7 +274,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.117.0) + gitaly-proto (0.118.1) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1006,7 +1004,6 @@ DEPENDENCIES diffy (~> 3.1.0) doorkeeper (~> 4.3) doorkeeper-openid_connect (~> 1.5) - dropzonejs-rails (~> 0.7.1) ed25519 (~> 1.2) email_reply_trimmer (~> 0.1) email_spec (~> 2.2.0) @@ -1031,7 +1028,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.117.0) + gitaly-proto (~> 0.118.1) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 17fab719008..30605e460e6 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -178,8 +178,6 @@ GEM doorkeeper-openid_connect (1.5.0) doorkeeper (~> 4.3) json-jwt (~> 1.6) - dropzonejs-rails (0.7.2) - rails (> 3.1) ed25519 (1.2.4) email_reply_trimmer (0.1.6) email_spec (2.2.0) @@ -279,7 +277,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.117.0) + gitaly-proto (0.118.1) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -1015,7 +1013,6 @@ DEPENDENCIES diffy (~> 3.1.0) doorkeeper (~> 4.3) doorkeeper-openid_connect (~> 1.5) - dropzonejs-rails (~> 0.7.1) ed25519 (~> 1.2) email_reply_trimmer (~> 0.1) email_spec (~> 2.2.0) @@ -1040,7 +1037,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.117.0) + gitaly-proto (~> 0.118.1) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index d3666cf3980..8b5536200e1 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -149,7 +149,8 @@ <a :href="issue.path" :title="issue.title" - class="js-no-trigger">{{ issue.title }}</a> + class="js-no-trigger" + @mousemove.stop>{{ issue.title }}</a> <span v-if="issueId" class="board-card-number append-right-5" diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 715dc1bfb42..a04d09ef374 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -50,7 +50,9 @@ export default { this.stopPipelinePolling(); }, methods: { - ...mapActions(['setRightPane']), + ...mapActions('rightPane', { + openRightPane: 'open', + }), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), startTimer() { this.intervalId = setInterval(() => { @@ -88,7 +90,7 @@ export default { <button type="button" class="p-0 border-0 h-50" - @click="setRightPane($options.rightSidebarViews.pipelines)" + @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon v-tooltip diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 75a9a9e9b8f..bd07f372177 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -1,5 +1,6 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; +import _ from 'underscore'; import { __ } from '~/locale'; import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; @@ -30,14 +31,10 @@ export default { }, }, computed: { - ...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']), + ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']), + ...mapState('rightPane', ['isOpen', 'currentView']), ...mapGetters(['packageJson']), - pipelinesActive() { - return ( - this.rightPane === rightSidebarViews.pipelines || - this.rightPane === rightSidebarViews.jobsDetail - ); - }, + ...mapGetters('rightPane', ['isActiveView', 'isAliveView']), showLivePreview() { return this.packageJson && this.clientsidePreviewEnabled; }, @@ -46,22 +43,26 @@ export default { { show: this.currentMergeRequestId, title: __('Merge Request'), - isActive: this.rightPane === rightSidebarViews.mergeRequestInfo, - view: rightSidebarViews.mergeRequestInfo, + views: [ + rightSidebarViews.mergeRequestInfo, + ], icon: 'text-description', }, { show: true, title: __('Pipelines'), - isActive: this.pipelinesActive, - view: rightSidebarViews.pipelines, + views: [ + rightSidebarViews.pipelines, + rightSidebarViews.jobsDetail, + ], icon: 'rocket', }, { show: this.showLivePreview, title: __('Live preview'), - isActive: this.rightPane === rightSidebarViews.clientSidePreview, - view: rightSidebarViews.clientSidePreview, + views: [ + rightSidebarViews.clientSidePreview, + ], icon: 'live-preview', }, ]; @@ -71,13 +72,26 @@ export default { .concat(this.extensionTabs) .filter(tab => tab.show); }, + tabViews() { + return _.flatten(this.tabs.map(tab => tab.views)); + }, + aliveTabViews() { + return this.tabViews.filter(view => this.isAliveView(view.name)); + }, }, methods: { - ...mapActions(['setRightPane']), - clickTab(e, view) { + ...mapActions('rightPane', ['toggleOpen', 'open']), + clickTab(e, tab) { e.target.blur(); - this.setRightPane(view); + if (this.isActiveTab(tab)) { + this.toggleOpen(); + } else { + this.open(tab.views[0]); + } + }, + isActiveTab(tab) { + return tab.views.some(view => this.isActiveView(view.name)); }, }, }; @@ -88,15 +102,22 @@ export default { class="multi-file-commit-panel ide-right-sidebar" > <resizable-panel - v-if="rightPane" + v-show="isOpen" :collapsible="false" :initial-width="350" :min-size="350" - :class="`ide-right-sidebar-${rightPane}`" + :class="`ide-right-sidebar-${currentView}`" side="right" class="multi-file-commit-panel-inner" > - <component :is="rightPane" /> + <div + v-for="tabView in aliveTabViews" + v-show="isActiveView(tabView.name)" + :key="tabView.name" + class="h-100" + > + <component :is="tabView.name" /> + </div> </resizable-panel> <nav class="ide-activity-bar"> <ul class="list-unstyled"> @@ -109,13 +130,13 @@ export default { :title="tab.title" :aria-label="tab.title" :class="{ - active: tab.isActive + active: isActiveTab(tab) && isOpen }" data-container="body" data-placement="left" class="ide-sidebar-link is-right" type="button" - @click="clickTab($event, tab.view)" + @click="clickTab($event, tab)" > <icon :size="16" diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index d3a73e84cc7..b2599128213 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -22,12 +22,14 @@ export default { }, }, computed: { + ...mapState('rightPane', { + rightPaneIsOpen: 'isOpen', + }), ...mapState([ 'rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView', - 'rightPane', ]), ...mapGetters([ 'currentMergeRequest', @@ -99,7 +101,7 @@ export default { this.editor.updateDimensions(); } }, - rightPane() { + rightPaneIsOpen() { this.editor.updateDimensions(); }, }, diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 8caa5b86a9b..3b201f006aa 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -29,10 +29,10 @@ export const diffModes = { }; export const rightSidebarViews = { - pipelines: 'pipelines-list', - jobsDetail: 'jobs-detail', - mergeRequestInfo: 'merge-request-info', - clientSidePreview: 'clientside', + pipelines: { name: 'pipelines-list', keepAlive: true }, + jobsDetail: { name: 'jobs-detail', keepAlive: false }, + mergeRequestInfo: { name: 'merge-request-info', keepAlive: true }, + clientSidePreview: { name: 'clientside', keepAlive: false }, }; export const stageKeys = { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index b8b64aead30..e10a132ab4b 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -184,10 +184,6 @@ export const burstUnusedSeal = ({ state, commit }) => { } }; -export const setRightPane = ({ commit }, view) => { - commit(types.SET_RIGHT_PANE, view); -}; - export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); export const setErrorMessage = ({ commit }, errorMessage) => diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 877d88bb060..f1f544b52b2 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -9,6 +9,7 @@ import pipelines from './modules/pipelines'; import mergeRequests from './modules/merge_requests'; import branches from './modules/branches'; import fileTemplates from './modules/file_templates'; +import paneModule from './modules/pane'; Vue.use(Vuex); @@ -24,6 +25,7 @@ export const createStore = () => mergeRequests, branches, fileTemplates: fileTemplates(), + rightPane: paneModule(), }, }); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index dd53213ed18..cc9f6c8638c 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -1,5 +1,6 @@ import Api from '~/api'; import { __ } from '~/locale'; +import { normalizeHeaders } from '~/lib/utils/common_utils'; import * as types from './mutation_types'; import eventHub from '../../../eventhub'; @@ -22,13 +23,21 @@ export const receiveTemplateTypesError = ({ commit, dispatch }) => { export const receiveTemplateTypesSuccess = ({ commit }, templates) => commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); -export const fetchTemplateTypes = ({ dispatch, state }) => { +export const fetchTemplateTypes = ({ dispatch, state }, page = 1) => { if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); dispatch('requestTemplateTypes'); - return Api.templates(state.selectedTemplateType.key) - .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data)) + return Api.templates(state.selectedTemplateType.key, { page }) + .then(({ data, headers }) => { + const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10); + + dispatch('receiveTemplateTypesSuccess', data); + + if (nextPage) { + dispatch('fetchTemplateTypes', nextPage); + } + }) .catch(() => dispatch('receiveTemplateTypesError')); }; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js index 674782a28ca..d519c033769 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -9,7 +9,7 @@ export default { }, [types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) { state.isLoading = false; - state.templates = templates; + state.templates = state.templates.concat(templates); }, [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { state.selectedTemplateType = type; diff --git a/app/assets/javascripts/ide/stores/modules/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js new file mode 100644 index 00000000000..7f5d167a14f --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/actions.js @@ -0,0 +1,30 @@ +import * as types from './mutation_types'; + +export const toggleOpen = ({ dispatch, state }, view) => { + if (state.isOpen) { + dispatch('close'); + } else { + dispatch('open', view); + } +}; + +export const open = ({ commit }, view) => { + commit(types.SET_OPEN, true); + + if (view) { + const { name, keepAlive } = view; + + commit(types.SET_CURRENT_VIEW, name); + + if (keepAlive) { + commit(types.KEEP_ALIVE_VIEW, name); + } + } +}; + +export const close = ({ commit }) => { + commit(types.SET_OPEN, false); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js new file mode 100644 index 00000000000..c346cf13689 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js @@ -0,0 +1,4 @@ +export const isActiveView = state => view => state.currentView === view; + +export const isAliveView = (state, getters) => view => + state.keepAliveViews[view] || (state.isOpen && getters.isActiveView(view)); diff --git a/app/assets/javascripts/ide/stores/modules/pane/index.js b/app/assets/javascripts/ide/stores/modules/pane/index.js new file mode 100644 index 00000000000..5f61cb732c8 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/index.js @@ -0,0 +1,12 @@ +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +export default () => ({ + namespaced: true, + state: state(), + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js new file mode 100644 index 00000000000..abdebc4d913 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_OPEN = 'SET_OPEN'; +export const SET_CURRENT_VIEW = 'SET_CURRENT_VIEW'; +export const KEEP_ALIVE_VIEW = 'KEEP_ALIVE_VIEW'; diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutations.js b/app/assets/javascripts/ide/stores/modules/pane/mutations.js new file mode 100644 index 00000000000..c16484b4402 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/mutations.js @@ -0,0 +1,19 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_OPEN](state, isOpen) { + Object.assign(state, { + isOpen, + }); + }, + [types.SET_CURRENT_VIEW](state, currentView) { + Object.assign(state, { + currentView, + }); + }, + [types.KEEP_ALIVE_VIEW](state, viewName) { + Object.assign(state.keepAliveViews, { + [viewName]: true, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/state.js b/app/assets/javascripts/ide/stores/modules/pane/state.js new file mode 100644 index 00000000000..353065b5735 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/state.js @@ -0,0 +1,5 @@ +export default () => ({ + isOpen: false, + currentView: null, + keepAliveViews: {}, +}); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 3e67b222e66..8fa86995ef0 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -113,7 +113,7 @@ export const toggleStageCollapsed = ({ commit }, stageId) => export const setDetailJob = ({ commit, dispatch }, job) => { commit(types.SET_DETAIL_JOB, job); - dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { root: true, }); }; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 5a7991d2fa7..a5f8098dc17 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; -export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; - export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 2c8535bda59..78cdfda74f0 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -166,11 +166,6 @@ export default { unusedSeal: false, }); }, - [types.SET_RIGHT_PANE](state, view) { - Object.assign(state, { - rightPane: state.rightPane === view ? null : view, - }); - }, [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 46b52fa00fc..d400b9831a9 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -23,7 +23,6 @@ export default () => ({ currentActivityView: activityBarViews.edit, unusedSeal: true, fileFindVisible: false, - rightPane: null, links: {}, errorMessage: null, entryModal: { diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 75dfdedcf1b..d08e8ba0c4b 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,10 +1,11 @@ import Vue from 'vue'; +import sanitize from 'sanitize-html'; import issuableApp from './components/app.vue'; import '../vue_shared/vue_resource_interceptor'; -document.addEventListener('DOMContentLoaded', () => { +export default function initIssueableApp() { const initialDataEl = document.getElementById('js-issuable-app-initial-data'); - const props = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); + const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"')); return new Vue({ el: document.getElementById('js-issuable-app'), @@ -17,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +} diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 74b3a515e84..ef65196872c 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -3,9 +3,10 @@ import Issue from '~/issue'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; -import '~/issue_show/index'; +import initIssueableApp from '~/issue_show'; export default function () { + initIssueableApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index f1ef50d0e3d..a07d63a495d 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -1,9 +1,11 @@ <script> +import { Link } from '@gitlab-org/gitlab-ui'; import Icon from '../../icon.vue'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { + 'gl-link': Link, Icon, }, props: { @@ -37,7 +39,7 @@ export default { ({{ fileSizeReadable }}) </template> </p> - <a + <gl-link :href="path" class="btn btn-default" rel="nofollow" @@ -49,7 +51,7 @@ export default { css-classes="float-left append-right-8" /> {{ __('Download') }} - </a> + </gl-link> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 08e102e57c3..ee3157bcb1b 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -18,12 +18,14 @@ */ +import { Link } from '@gitlab-org/gitlab-ui'; import userAvatarImage from './user_avatar_image.vue'; import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', components: { + 'gl-link': Link, userAvatarImage, }, directives: { @@ -83,7 +85,7 @@ export default { </script> <template> - <a + <gl-link :href="linkHref" class="user-avatar-link"> <user-avatar-image @@ -99,5 +101,5 @@ export default { :title="tooltipText" :tooltip-placement="tooltipPlacement" >{{ username }}</span> - </a> + </gl-link> </template> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index f2950308019..ffe65ce780e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -5,7 +5,6 @@ *= require jquery.atwho *= require select2 *= require_self - *= require dropzone/basic *= require cropper.css */ @@ -18,6 +17,7 @@ */ @import "../../../node_modules/pikaday/scss/pikaday"; +@import "../../../node_modules/dropzone/dist/basic.css"; /* * GitLab UI framework diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 28dda65091d..3c9505a21d6 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -324,6 +324,16 @@ img.emoji { word-wrap: break-word; } +.checkbox-icon-inline-wrapper { + .checkbox { + display: inline; + + label { + display: inline; + } + } +} + /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-2 { margin-top: 2px; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 7d53a631cdf..f10eaedcc04 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -59,7 +59,7 @@ } @include media-breakpoint-up(sm) { - .btn:first-of-type { + .btn:nth-child(1) { margin-left: auto; } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b87034d10b6..d7dbc712743 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -109,6 +109,15 @@ class ApplicationController < ActionController::Base request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay'] end + def render(*args) + super.tap do + # Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse + if response.content_type == 'text/html' && (400..599).cover?(response.status) + response.headers['X-GitLab-Custom-Error'] = '1' + end + end + end + protected def append_info_to_payload(payload) diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 86583adc6a4..5639402a1e9 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -106,6 +106,10 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @commits = set_commits_for_rendering(@merge_request.commits) @commit = @merge_request.diff_head_commit + # FIXME: We have to assign a presenter to another instance variable + # due to class_name checks being made with issuable classes + @mr_presenter = @merge_request.present(current_user: current_user) + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute set_pipeline_variables diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dfb69de650b..d691744d72a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -333,6 +333,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @target_project = @merge_request.target_project @target_branches = @merge_request.target_project.repository.branch_names @noteable = @merge_request + + # FIXME: We have to assign a presenter to another instance variable + # due to class_name checks being made with issuable classes + @mr_presenter = @merge_request.present(current_user: current_user) end def finder_type diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7352c5e9bec..a9417369ca2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -16,6 +16,7 @@ class ProjectsController < Projects::ApplicationController before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export] + before_action :present_project, only: [:edit] # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] @@ -433,4 +434,8 @@ class ProjectsController < Projects::ApplicationController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440') end + + def present_project + @project = @project.present(current_user: current_user) + end end diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index fd7aeca0d8b..2e82bda8730 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -12,6 +12,7 @@ class EventsFinder # Arguments: # source - which user or project to looks for events on # current_user - only return events for projects visible to this user + # WARNING: does not consider project feature visibility! # params: # action: string # target_type: string diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb index 18cc6891ca4..4d8128dd824 100644 --- a/app/finders/joined_groups_finder.rb +++ b/app/finders/joined_groups_finder.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class JoinedGroupsFinder < UnionFinder +class JoinedGroupsFinder def initialize(user) @user = user end @@ -8,19 +8,8 @@ class JoinedGroupsFinder < UnionFinder # Finds the groups of the source user, optionally limited to those visible to # the current user. def execute(current_user = nil) - segments = all_groups(current_user) - - find_union(segments, Group).order_id_desc - end - - private - - def all_groups(current_user) - groups = [] - - groups << @user.authorized_groups.visible_to_user(current_user) if current_user - groups << @user.authorized_groups.public_to_user(current_user) - - groups + @user.authorized_groups + .public_or_visible_to_user(current_user) + .order_id_desc end end diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index a4daf5b5841..eeca5026da1 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -3,6 +3,7 @@ # Get user activity feed for projects common for a user and a logged in user # # - current_user: The user viewing the events +# WARNING: does not consider project feature visibility! # - user: The user for which to load the events # - params: # - offset: The page of events to return diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 321811a3ca3..7fc4c1a023f 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module AvatarsHelper - def project_icon(project_id, options = {}) - source_icon(Project, project_id, options) + def project_icon(project, options = {}) + source_icon(project, options) end - def group_icon(group_id, options = {}) - source_icon(Group, group_id, options) + def group_icon(group, options = {}) + source_icon(group, options) end # Takes both user and email and returns the avatar_icon by @@ -110,16 +110,11 @@ module AvatarsHelper private - def source_icon(klass, source_id, options = {}) - source = - if source_id.respond_to?(:avatar_url) - source_id - else - klass.find_by_full_path(source_id) - end + def source_icon(source, options = {}) + avatar_url = source.try(:avatar_url) - if source.avatar_url - image_tag source.avatar_url, options + if avatar_url + image_tag avatar_url, options else source_identicon(source, options) end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 67af0a4eb98..be085496731 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -3,13 +3,14 @@ module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) - setup_merge_request_mail(merge_request_id, recipient_id) + setup_merge_request_mail(merge_request_id, recipient_id, present: true) mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason)) end def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) - setup_merge_request_mail(merge_request_id, recipient_id) + setup_merge_request_mail(merge_request_id, recipient_id, present: true) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end @@ -75,11 +76,16 @@ module Emails private - def setup_merge_request_mail(merge_request_id, recipient_id) + def setup_merge_request_mail(merge_request_id, recipient_id, present: false) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project @target_url = project_merge_request_url(@project, @merge_request) + if present + recipient = User.find(recipient_id) + @mr_presenter = @merge_request.present(current_user: recipient) + end + @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb index d12dd93ce2e..7cae60a74d6 100644 --- a/app/models/blob_viewer/package_json.rb +++ b/app/models/blob_viewer/package_json.rb @@ -33,7 +33,8 @@ module BlobViewer end def homepage - json_data['homepage'] + url = json_data['homepage'] + url if Gitlab::UrlSanitizer.valid?(url) end def npm_url diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 017ec0b145a..08514d6af4e 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -10,5 +10,9 @@ module Ci alias_attribute :secret_value, :value validates :key, uniqueness: { scope: :pipeline_id } + + def hook_attrs + { key: key, value: value } + end end end diff --git a/app/models/event.rb b/app/models/event.rb index 596155a9525..2e690f8c013 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -148,6 +148,8 @@ class Event < ActiveRecord::Base end end + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity def visible_to_user?(user = nil) if push? || commit_note? Ability.allowed?(user, :download_code, project) @@ -159,12 +161,18 @@ class Event < ActiveRecord::Base Ability.allowed?(user, :read_issue, note? ? note_target : target) elsif merge_request? || merge_request_note? Ability.allowed?(user, :read_merge_request, note? ? note_target : target) + elsif personal_snippet_note? + Ability.allowed?(user, :read_personal_snippet, note_target) + elsif project_snippet_note? + Ability.allowed?(user, :read_project_snippet, note_target) elsif milestone? - Ability.allowed?(user, :read_project, project) + Ability.allowed?(user, :read_milestone, project) else false # No other event types are visible end end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity def project_name if project @@ -306,6 +314,10 @@ class Event < ActiveRecord::Base note? && target && target.for_snippet? end + def personal_snippet_note? + note? && target && target.for_personal_snippet? + end + def note_target target.noteable end diff --git a/app/models/group.rb b/app/models/group.rb index 62af20d2142..612c546ca57 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -82,8 +82,17 @@ class Group < Namespace User.reference_pattern end - def visible_to_user(user) - where(id: user.authorized_groups.select(:id).reorder(nil)) + # WARNING: This method should never be used on its own + # please do make sure the number of rows you are filtering is small + # enough for this query + def public_or_visible_to_user(user) + return public_to_user unless user + + public_for_user = public_to_user_arel(user) + visible_for_user = visible_to_user_arel(user) + public_or_visible = public_for_user.or(visible_for_user) + + where(public_or_visible) end def select_for_project_authorization @@ -95,6 +104,23 @@ class Group < Namespace super end end + + private + + def public_to_user_arel(user) + self.arel_table[:visibility_level] + .in(Gitlab::VisibilityLevel.levels_for_user(user)) + end + + def visible_to_user_arel(user) + groups_table = self.arel_table + authorized_groups = user.authorized_groups.as('authorized') + + groups_table.project(1) + .from(authorized_groups) + .where(authorized_groups[:id].eq(groups_table[:id])) + .exists + end end # Overrides notification_settings has_many association diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 771a61b090f..68ba4b213b2 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -3,6 +3,16 @@ class WebHook < ActiveRecord::Base include Sortable + attr_encrypted :token, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + + attr_encrypted :url, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?), @@ -27,4 +37,38 @@ class WebHook < ActiveRecord::Base def allow_local_requests? false end + + # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. + # Ensure that the encrypted version always takes precedence if present. + alias_method :attr_encrypted_token, :token + def token + attr_encrypted_token.presence || read_attribute(:token) + end + + # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. + # Pending a background migration to encrypt all fields, we should just clear + # the unencrypted value whenever the new value is set. + alias_method :'attr_encrypted_token=', :'token=' + def token=(value) + self.attr_encrypted_token = value + + write_attribute(:token, nil) + end + + # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. + # Ensure that the encrypted version always takes precedence if present. + alias_method :attr_encrypted_url, :url + def url + attr_encrypted_url.presence || read_attribute(:url) + end + + # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. + # Pending a background migration to encrypt all fields, we should just clear + # the unencrypted value whenever the new value is set. + alias_method :'attr_encrypted_url=', :'url=' + def url=(value) + self.attr_encrypted_url = value + + write_attribute(:url, nil) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dd5d494997d..0481a4a3d28 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base include Issuable include Noteable include Referable + include Presentable include IgnorableColumn include TimeTrackable include ManualInverseAssociation diff --git a/app/models/repository.rb b/app/models/repository.rb index 12fbf7d5d1d..a3a3ce179fc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -510,7 +510,7 @@ class Repository raw_repository.exists? end - cache_method :exists? + cache_method_asymmetrically :exists? # We don't need to cache the output of this method because both exists? and # has_visible_content? are already memoized and cached. There's no guarantee @@ -612,7 +612,7 @@ class Repository Licensee::License.new(license_key) end - cache_method :license, memoize_only: true + memoize_method :license def gitignore file_on_head(:gitignore) @@ -668,6 +668,14 @@ class Repository end end + def list_last_commits_for_tree(sha, path, offset: 0, limit: 25) + commits = raw_repository.list_last_commits_for_tree(sha, path, offset: offset, limit: limit) + + commits.each do |path, commit| + commits[path] = ::Commit.new(commit, @project) + end + end + def last_commit_for_path(sha, path) commit = raw_repository.last_commit_for_path(sha, path) ::Commit.new(commit, @project) if commit @@ -1029,6 +1037,10 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end + def request_store_cache + @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) + end + def tags_sorted_by_committed_date tags.sort_by do |tag| # Annotated tags can point to any object (e.g. a blob), but generally diff --git a/app/models/user.rb b/app/models/user.rb index eeac87e2e52..cd3b1c95b7e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -674,10 +674,12 @@ class User < ActiveRecord::Base # Returns the groups a user has access to, either through a membership or a project authorization def authorized_groups - Group.from_union([ - groups, - authorized_projects.joins(:namespace).select('namespaces.*') - ]) + Group.unscoped do + Group.from_union([ + groups, + authorized_projects.joins(:namespace).select('namespaces.*') + ]) + end end # Returns the groups a user is a member of, either directly or through a parent group diff --git a/app/serializers/diff_line_entity.rb b/app/serializers/diff_line_entity.rb index 2119a1017d3..942714b7787 100644 --- a/app/serializers/diff_line_entity.rb +++ b/app/serializers/diff_line_entity.rb @@ -9,6 +9,6 @@ class DiffLineEntity < Grape::Entity expose :meta_positions, as: :meta_data expose :rich_text do |line| - line.rich_text || CGI.escapeHTML(line.text) + ERB::Util.html_escape(line.rich_text || line.text) end end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 35f5cff0e0c..5017fa093f3 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -14,8 +14,8 @@ module Clusters else check_timeout end - rescue Kubeclient::HttpError => ke - app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? + rescue Kubeclient::HttpError + app.make_errored!("Kubernetes error") unless app.errored? end private @@ -27,7 +27,7 @@ module Clusters end def on_failed - app.make_errored!(installation_errors || 'Installation silently failed') + app.make_errored!('Installation failed') ensure remove_installation_pod end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 7e3c0e77a83..dd8d2ed5eb6 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -12,10 +12,10 @@ module Clusters ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) - rescue Kubeclient::HttpError => ke - app.make_errored!("Kubernetes error: #{ke.message}") - rescue StandardError => e - app.make_errored!("Can't start installation process. #{e.message}") + rescue Kubeclient::HttpError + app.make_errored!("Kubernetes error.") + rescue StandardError + app.make_errored!("Can't start installation process.") end end end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 9417c63c43a..de6ff92d1da 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -55,7 +55,6 @@ module Users :force_random_password, :hide_no_password, :hide_no_ssh_key, - :key_id, :linkedin, :name, :password, @@ -69,7 +68,10 @@ module Users :twitter, :username, :website_url, - :private_profile + :private_profile, + :organization, + :location, + :public_email ] end diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 7a66bac09cb..198c2d35b29 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -7,8 +7,7 @@ GitLab Community Edition - if user_signed_in? - %span= Gitlab::VERSION - %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision) + %span= link_to Gitlab::VERSION, Gitlab::COM_URL + namespace_project_tag_path('gitlab-org', 'gitlab-ce', "v#{Gitlab::VERSION}") = version_status_badge %hr diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index dd6a84e503d..5acd45b74a7 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -9,7 +9,7 @@ %p Assignee: #{@merge_request.assignee_name} -= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request += render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter - if @merge_request.description %div diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index d5b8f8d764f..754f4bca1cd 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= @merge_request.author_name %> Assignee: <%= @merge_request.assignee_name %> -<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %> +<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %> <%= @merge_request.description %> diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 51f5ecf2166..ea215e3e718 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -109,10 +109,11 @@ = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.") %hr %h5= ("Private profile") - - private_profile_label = capture do - = s_("Profiles|Don't display activity-related personal information on your profiles") + .checkbox-icon-inline-wrapper + - private_profile_label = capture do + = s_("Profiles|Don't display activity-related personal information on your profiles") + = f.check_box :private_profile, label: private_profile_label = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile') - = f.check_box :private_profile, label: private_profile_label %h5= s_("Profiles|Private contributions") = f.check_box :include_private_contributions, label: 'Include private contributions on my profile' .help-block diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index bfd165d8ba5..07fc9e1c682 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -47,7 +47,7 @@ .form-group - if @project.avatar? .avatar-container.s160.append-bottom-15 - = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) + = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) - if @project.avatar_in_git %p.light = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d104608b2fe..75f35360e5e 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -9,7 +9,7 @@ .project-empty-note-panel %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 - %h4 + %h4.append-bottom-20 = _('The repository for this project is empty') - if @project.can_current_user_push_code? diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml index 12cf40bb65f..a69146513d8 100644 --- a/app/views/projects/forks/_fork_button.html.haml +++ b/app/views/projects/forks/_fork_button.html.haml @@ -5,7 +5,7 @@ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked = link_to project_path(forked_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_icon(namespace, class: "avatar s100 identicon") + = group_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") @@ -18,7 +18,7 @@ class: ("disabled has-tooltip" unless can_create_project), title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_icon(namespace, class: "avatar s100 identicon") + = group_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 5a59f956cb5..13b967beba1 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' }, data: { markdown_version: @merge_request.cached_markdown_version } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request, presenter: @mr_presenter diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index d5c4134dee2..464f8fa65e9 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -11,7 +11,7 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits + = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter = f.hidden_field :source_project_id = f.hidden_field :source_branch = f.hidden_field :target_project_id diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5b28a43a361..b33c758b464 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,6 +1,7 @@ - form = local_assigns.fetch(:f) - commits = local_assigns[:commits] - project = @target_project || @project +- presenter = local_assigns.fetch(:presenter, nil) = form_errors(issuable) @@ -29,7 +30,7 @@ = render 'shared/issuable/form/metadata', issuable: issuable, form: form -= render_if_exists 'shared/issuable/approvals', issuable: issuable, form: form += render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index af29c0fe59e..2682d92fc56 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -22,7 +22,7 @@ %strong Blocked - if user.two_factor_enabled? - %label.label.label-info + %label.badge.badge-info 2FA - if source.instance_of?(Group) && source != @group diff --git a/changelogs/unreleased/-48445-Issue-card-in-board-view-is-too-sensitive-to-drag-event.yml b/changelogs/unreleased/-48445-Issue-card-in-board-view-is-too-sensitive-to-drag-event.yml new file mode 100644 index 00000000000..27c70318b92 --- /dev/null +++ b/changelogs/unreleased/-48445-Issue-card-in-board-view-is-too-sensitive-to-drag-event.yml @@ -0,0 +1,5 @@ +--- +title: Fix link handling for issue cards to avoid too sensitive drag events. +merge_request: 21910 +author: Johann Hubert Sonntagbauer +type: fixed diff --git a/changelogs/unreleased/23197-add-custom-header-for-error-responses.yml b/changelogs/unreleased/23197-add-custom-header-for-error-responses.yml new file mode 100644 index 00000000000..a5ffc197a0c --- /dev/null +++ b/changelogs/unreleased/23197-add-custom-header-for-error-responses.yml @@ -0,0 +1,6 @@ +--- +title: Set a header for custom error pages to prevent them from being intercepted + by gitlab-workhorse +merge_request: 21870 +author: David Piegza +type: fixed diff --git a/changelogs/unreleased/37433-solve-n-1-in-refs-controller-logs-tree.yml b/changelogs/unreleased/37433-solve-n-1-in-refs-controller-logs-tree.yml new file mode 100644 index 00000000000..04662a7cfe2 --- /dev/null +++ b/changelogs/unreleased/37433-solve-n-1-in-refs-controller-logs-tree.yml @@ -0,0 +1,6 @@ +--- +title: Adds support for Gitaly ListLastCommitsForTree RPC in order to make bulk-fetch + of commits more performant +merge_request: 21921 +author: +type: performance diff --git a/changelogs/unreleased/45453-fix-delete-protected-branch-btn.yml b/changelogs/unreleased/45453-fix-delete-protected-branch-btn.yml new file mode 100644 index 00000000000..64776abdc07 --- /dev/null +++ b/changelogs/unreleased/45453-fix-delete-protected-branch-btn.yml @@ -0,0 +1,5 @@ +--- +title: Fixes modal button alignment +merge_request: 22024 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/48399-skip-auto-devops-jobs-based-on-license.yml b/changelogs/unreleased/48399-skip-auto-devops-jobs-based-on-license.yml new file mode 100644 index 00000000000..042731fb9be --- /dev/null +++ b/changelogs/unreleased/48399-skip-auto-devops-jobs-based-on-license.yml @@ -0,0 +1,6 @@ +--- +title: Skip creating auto devops jobs for sast, container_scanning, dast, dependency_scanning + when not licensed +merge_request: 21959 +author: +type: performance diff --git a/changelogs/unreleased/51021-more-attr-encrypted.yml b/changelogs/unreleased/51021-more-attr-encrypted.yml new file mode 100644 index 00000000000..0e18c59f1bb --- /dev/null +++ b/changelogs/unreleased/51021-more-attr-encrypted.yml @@ -0,0 +1,5 @@ +--- +title: Encrypt webhook tokens and URLs in the database +merge_request: 21645 +author: +type: security diff --git a/changelogs/unreleased/51404-update-groups-and-projects-api-docs.yml b/changelogs/unreleased/51404-update-groups-and-projects-api-docs.yml new file mode 100644 index 00000000000..da02a91041b --- /dev/null +++ b/changelogs/unreleased/51404-update-groups-and-projects-api-docs.yml @@ -0,0 +1,5 @@ +--- +title: Docs for Project/Groups members API with inherited members +merge_request: 21984 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/51476-private-profile-help-url-should-not-toggle-checkbox.yml b/changelogs/unreleased/51476-private-profile-help-url-should-not-toggle-checkbox.yml new file mode 100644 index 00000000000..d4e4503508d --- /dev/null +++ b/changelogs/unreleased/51476-private-profile-help-url-should-not-toggle-checkbox.yml @@ -0,0 +1,5 @@ +--- +title: Prevents private profile help link from toggling checkbox +merge_request: 21757 +author: +type: other diff --git a/changelogs/unreleased/51651-fill-pipeline-source-for-external-pipelines.yml b/changelogs/unreleased/51651-fill-pipeline-source-for-external-pipelines.yml new file mode 100644 index 00000000000..6720430e5fc --- /dev/null +++ b/changelogs/unreleased/51651-fill-pipeline-source-for-external-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Retroactively fill pipeline source for external pipelines. +merge_request: 21814 +author: +type: other diff --git a/changelogs/unreleased/add-gl-link-to-download-viewer.yml b/changelogs/unreleased/add-gl-link-to-download-viewer.yml new file mode 100644 index 00000000000..ce3d916f045 --- /dev/null +++ b/changelogs/unreleased/add-gl-link-to-download-viewer.yml @@ -0,0 +1,5 @@ +--- +title: Add link component to DownloadViewer component +merge_request: 21987 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/add-gl-link-to-user-avatar-link.yml b/changelogs/unreleased/add-gl-link-to-user-avatar-link.yml new file mode 100644 index 00000000000..ef87ef541dd --- /dev/null +++ b/changelogs/unreleased/add-gl-link-to-user-avatar-link.yml @@ -0,0 +1,5 @@ +--- +title: Add link component to UserAvatarLink component +merge_request: 21986 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/bvl-remove-sha-from-help.yml b/changelogs/unreleased/bvl-remove-sha-from-help.yml new file mode 100644 index 00000000000..37f797f98f4 --- /dev/null +++ b/changelogs/unreleased/bvl-remove-sha-from-help.yml @@ -0,0 +1,5 @@ +--- +title: Link to the tag for a version on the help page instead of to the commit +merge_request: 22015 +author: +type: changed diff --git a/changelogs/unreleased/feature-set-public-email-through-api.yml b/changelogs/unreleased/feature-set-public-email-through-api.yml new file mode 100644 index 00000000000..22fae71e9d8 --- /dev/null +++ b/changelogs/unreleased/feature-set-public-email-through-api.yml @@ -0,0 +1,5 @@ +--- +title: Add support for setting the public email through the api +merge_request: 21938 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-add-organization-and-location-to-allowed-parameters.yml b/changelogs/unreleased/fix-add-organization-and-location-to-allowed-parameters.yml new file mode 100644 index 00000000000..4d85e1b9af2 --- /dev/null +++ b/changelogs/unreleased/fix-add-organization-and-location-to-allowed-parameters.yml @@ -0,0 +1,5 @@ +--- +title: Allow setting user's organization and location attributes through the API by adding them to the list of allowed parameters +merge_request: 21938 +author: Alexis Reigel +type: fixed diff --git a/changelogs/unreleased/fix-events-finder-incomplete.yml b/changelogs/unreleased/fix-events-finder-incomplete.yml new file mode 100644 index 00000000000..f3a4e421d33 --- /dev/null +++ b/changelogs/unreleased/fix-events-finder-incomplete.yml @@ -0,0 +1,5 @@ +--- +title: Redact confidential events in the API +merge_request: +author: +type: security diff --git a/changelogs/unreleased/frozen-string-app-enforce.yml b/changelogs/unreleased/frozen-string-app-enforce.yml new file mode 100644 index 00000000000..44686557c45 --- /dev/null +++ b/changelogs/unreleased/frozen-string-app-enforce.yml @@ -0,0 +1,5 @@ +--- +title: Check frozen string in style builds +merge_request: +author: gfyoung +type: other diff --git a/changelogs/unreleased/frozen-string-docs.yml b/changelogs/unreleased/frozen-string-docs.yml new file mode 100644 index 00000000000..2eb8a446ee8 --- /dev/null +++ b/changelogs/unreleased/frozen-string-docs.yml @@ -0,0 +1,5 @@ +--- +title: Update docs regarding frozen string +merge_request: +author: gfyoung +type: other diff --git a/changelogs/unreleased/ide-fetch-templates-pages.yml b/changelogs/unreleased/ide-fetch-templates-pages.yml new file mode 100644 index 00000000000..d4703e530f2 --- /dev/null +++ b/changelogs/unreleased/ide-fetch-templates-pages.yml @@ -0,0 +1,5 @@ +--- +title: Fixed file templates not fully being fetched in Web IDE +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/improve-empty-project-placeholder.yml b/changelogs/unreleased/improve-empty-project-placeholder.yml new file mode 100644 index 00000000000..11fe21e7710 --- /dev/null +++ b/changelogs/unreleased/improve-empty-project-placeholder.yml @@ -0,0 +1,5 @@ +--- +title: Improve empty project placeholder for non-members and members without write access +merge_request: 21977 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/lib-api-frozen-string-enable.yml b/changelogs/unreleased/lib-api-frozen-string-enable.yml new file mode 100644 index 00000000000..eb59f0bc2d1 --- /dev/null +++ b/changelogs/unreleased/lib-api-frozen-string-enable.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string in lib/api and lib/backup +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/mk-asymmetric-exists-cache.yml b/changelogs/unreleased/mk-asymmetric-exists-cache.yml new file mode 100644 index 00000000000..b6eec7d1fc6 --- /dev/null +++ b/changelogs/unreleased/mk-asymmetric-exists-cache.yml @@ -0,0 +1,6 @@ +--- +title: 'Resolve "Geo: Does not mark repositories as missing on primary due to stale + cache"' +merge_request: 21789 +author: +type: fixed diff --git a/changelogs/unreleased/pipeline-event-variables.yml b/changelogs/unreleased/pipeline-event-variables.yml new file mode 100644 index 00000000000..90fd964efd5 --- /dev/null +++ b/changelogs/unreleased/pipeline-event-variables.yml @@ -0,0 +1,5 @@ +--- +title: pipeline webhook event now contain pipeline variables +merge_request: 18171 +author: Pierre Tardy +type: added diff --git a/changelogs/unreleased/rename-local-variable.yml b/changelogs/unreleased/rename-local-variable.yml new file mode 100644 index 00000000000..70281dfef08 --- /dev/null +++ b/changelogs/unreleased/rename-local-variable.yml @@ -0,0 +1,5 @@ +--- +title: Rename block scope local variable in table pagination spec +merge_request: 21969 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/security-2697-code-highlight-timeout.yml b/changelogs/unreleased/security-2697-code-highlight-timeout.yml new file mode 100644 index 00000000000..66ad9ff822b --- /dev/null +++ b/changelogs/unreleased/security-2697-code-highlight-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Set timeout for syntax highlighting +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-acet-issue-details.yml b/changelogs/unreleased/security-acet-issue-details.yml new file mode 100644 index 00000000000..64147a9d6e8 --- /dev/null +++ b/changelogs/unreleased/security-acet-issue-details.yml @@ -0,0 +1,5 @@ +--- +title: Sanitize JSON data properly to fix XSS on Issue details page +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fj-stored-xss-in-repository-imports.yml b/changelogs/unreleased/security-fj-stored-xss-in-repository-imports.yml new file mode 100644 index 00000000000..7520aa624c7 --- /dev/null +++ b/changelogs/unreleased/security-fj-stored-xss-in-repository-imports.yml @@ -0,0 +1,5 @@ +--- +title: Fix stored XSS in merge requests from imported repository +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-package-json-xss.yml b/changelogs/unreleased/security-package-json-xss.yml new file mode 100644 index 00000000000..6ab4854e44f --- /dev/null +++ b/changelogs/unreleased/security-package-json-xss.yml @@ -0,0 +1,5 @@ +--- +title: Fix xss vulnerability sourced from package.json +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-forks-with-no-gravatar.yml b/changelogs/unreleased/sh-fix-forks-with-no-gravatar.yml new file mode 100644 index 00000000000..f18e6207b87 --- /dev/null +++ b/changelogs/unreleased/sh-fix-forks-with-no-gravatar.yml @@ -0,0 +1,5 @@ +--- +title: Fix Error 500 when forking projects with Gravatar disabled +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-issue-52009.yml b/changelogs/unreleased/sh-fix-issue-52009.yml new file mode 100644 index 00000000000..fc22a58a66a --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-52009.yml @@ -0,0 +1,5 @@ +--- +title: Prevent Error 500s with invalid relative links +merge_request: 22001 +author: +type: fixed diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb index 12a0770a455..07b06629dea 100644 --- a/config/initializers/postgresql_opclasses_support.rb +++ b/config/initializers/postgresql_opclasses_support.rb @@ -208,5 +208,9 @@ module ActiveRecord index_parts << "comment: #{index.comment.inspect}" if Gitlab.rails5? && index.comment index_parts end + + def format_options(options) + options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") + end end end diff --git a/db/migrate/20170222111732_create_gpg_keys.rb b/db/migrate/20170222111732_create_gpg_keys.rb index 541228e8735..0d6d454bbf3 100644 --- a/db/migrate/20170222111732_create_gpg_keys.rb +++ b/db/migrate/20170222111732_create_gpg_keys.rb @@ -1,4 +1,6 @@ class CreateGpgKeys < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false def change @@ -12,8 +14,8 @@ class CreateGpgKeys < ActiveRecord::Migration t.text :key - t.index :primary_keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil - t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil + t.index :primary_keyid, unique: true, length: mysql_compatible_index_length + t.index :fingerprint, unique: true, length: mysql_compatible_index_length end end end diff --git a/db/migrate/20170613154149_create_gpg_signatures.rb b/db/migrate/20170613154149_create_gpg_signatures.rb index f6b5e7ebb7b..abef13a7a0b 100644 --- a/db/migrate/20170613154149_create_gpg_signatures.rb +++ b/db/migrate/20170613154149_create_gpg_signatures.rb @@ -1,4 +1,6 @@ class CreateGpgSignatures < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false def change @@ -16,8 +18,8 @@ class CreateGpgSignatures < ActiveRecord::Migration t.text :gpg_key_user_name t.text :gpg_key_user_email - t.index :commit_sha, unique: true, length: Gitlab::Database.mysql? ? 20 : nil - t.index :gpg_key_primary_keyid, length: Gitlab::Database.mysql? ? 20 : nil + t.index :commit_sha, unique: true, length: mysql_compatible_index_length + t.index :gpg_key_primary_keyid, length: mysql_compatible_index_length end end end diff --git a/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb b/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb index 1b360b231a8..2140ff7b05e 100644 --- a/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb +++ b/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb @@ -8,7 +8,7 @@ class AddIndexOnMergeRequestDiffCommitSha < ActiveRecord::Migration disable_ddl_transaction! def up - add_concurrent_index :merge_request_diff_commits, :sha, length: Gitlab::Database.mysql? ? 20 : nil + add_concurrent_index :merge_request_diff_commits, :sha, length: mysql_compatible_index_length end def down diff --git a/db/migrate/20170927161718_create_gpg_key_subkeys.rb b/db/migrate/20170927161718_create_gpg_key_subkeys.rb index c03c40416a8..d9dc2404cac 100644 --- a/db/migrate/20170927161718_create_gpg_key_subkeys.rb +++ b/db/migrate/20170927161718_create_gpg_key_subkeys.rb @@ -1,4 +1,6 @@ class CreateGpgKeySubkeys < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false def up @@ -8,8 +10,8 @@ class CreateGpgKeySubkeys < ActiveRecord::Migration t.binary :keyid t.binary :fingerprint - t.index :keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil - t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil + t.index :keyid, unique: true, length: mysql_compatible_index_length + t.index :fingerprint, unique: true, length: mysql_compatible_index_length end add_reference :gpg_signatures, :gpg_key_subkey, index: true, foreign_key: { on_delete: :nullify } diff --git a/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb new file mode 100644 index 00000000000..72f5c8d653b --- /dev/null +++ b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddAttrEncryptedColumnsToWebHook < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :web_hooks, :encrypted_token, :string + add_column :web_hooks, :encrypted_token_iv, :string + + add_column :web_hooks, :encrypted_url, :string + add_column :web_hooks, :encrypted_url_iv, :string + end +end diff --git a/db/migrate/20180916011959_add_index_pipelines_project_id_source.rb b/db/migrate/20180916011959_add_index_pipelines_project_id_source.rb new file mode 100644 index 00000000000..b9bebf30cf0 --- /dev/null +++ b/db/migrate/20180916011959_add_index_pipelines_project_id_source.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexPipelinesProjectIdSource < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, [:project_id, :source] + end + + def down + remove_concurrent_index :ci_pipelines, [:project_id, :source] + end +end diff --git a/db/post_migrate/20180914162043_encrypt_web_hooks_columns.rb b/db/post_migrate/20180914162043_encrypt_web_hooks_columns.rb new file mode 100644 index 00000000000..05ec4864a9e --- /dev/null +++ b/db/post_migrate/20180914162043_encrypt_web_hooks_columns.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class EncryptWebHooksColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + BATCH_SIZE = 10000 + RANGE_SIZE = 100 + MIGRATION = 'EncryptColumns' + COLUMNS = [:token, :url] + + WebHook = ::Gitlab::BackgroundMigration::Models::EncryptColumns::WebHook + + disable_ddl_transaction! + + def up + WebHook.each_batch(of: BATCH_SIZE) do |relation, index| + delay = index * 2.minutes + + relation.each_batch(of: RANGE_SIZE) do |relation| + range = relation.pluck('MIN(id)', 'MAX(id)').first + args = [WebHook, COLUMNS, *range] + + BackgroundMigrationWorker.perform_in(delay, MIGRATION, args) + end + end + end + + def down + # noop + end +end diff --git a/db/post_migrate/20180916014356_populate_external_pipeline_source.rb b/db/post_migrate/20180916014356_populate_external_pipeline_source.rb new file mode 100644 index 00000000000..5577d05cf40 --- /dev/null +++ b/db/post_migrate/20180916014356_populate_external_pipeline_source.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateExternalPipelineSource < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + MIGRATION = 'PopulateExternalPipelineSource'.freeze + BATCH_SIZE = 500 + + disable_ddl_transaction! + + class Pipeline < ActiveRecord::Base + include EachBatch + self.table_name = 'ci_pipelines' + end + + def up + Pipeline.where(source: nil).tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index f92d8005dfb..13c6d65255e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -475,6 +475,7 @@ ActiveRecord::Schema.define(version: 20180917172041) do add_index "ci_pipelines", ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree + add_index "ci_pipelines", ["project_id", "source"], name: "index_ci_pipelines_on_project_id_and_source", using: :btree add_index "ci_pipelines", ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source", using: :btree add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree @@ -2271,6 +2272,10 @@ ActiveRecord::Schema.define(version: 20180917172041) do t.boolean "job_events", default: false, null: false t.boolean "confidential_note_events" t.text "push_events_branch_filter" + t.string "encrypted_token" + t.string "encrypted_token_iv" + t.string "encrypted_url" + t.string "encrypted_url_iv" end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree diff --git a/doc/api/members.md b/doc/api/members.md index 7b228b92594..bb4fae35f52 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -15,6 +15,7 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these l ## List all members of a group or project Gets a list of group or project members viewable by the authenticated user. +Returns only direct members and not inherited members through ancestors groups. ``` GET /groups/:id/members @@ -61,6 +62,7 @@ Example response: ## List all members of a group or project including inherited members Gets a list of group or project members viewable by the authenticated user, including inherited members through ancestor groups. +Returns multiple times the same user (with different member attributes) when the user is a member of the project/group and of one or more ancestor group. ``` GET /groups/:id/members/all diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 49fb9bc141d..0623a6b02ae 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -90,6 +90,8 @@ Like [Get file from repository](repository_files.md#get-file-from-repository) yo ## Create new file in repository +This allows you to create a single file. For creating multiple files with a single request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions). + ``` POST /projects/:id/repository/files/:file_path ``` @@ -120,6 +122,8 @@ Parameters: ## Update existing file in repository +This allows you to update a single file. For updating multiple files with a single request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions). + ``` PUT /projects/:id/repository/files/:file_path ``` @@ -160,6 +164,8 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify ## Delete existing file in repository +This allows you to delete a single file. For deleting multiple files with a singleh request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions). + ``` DELETE /projects/:id/repository/files/:file_path ``` diff --git a/doc/api/users.md b/doc/api/users.md index 762ea53edee..3b41e0f7ec6 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -288,6 +288,7 @@ Parameters: - `provider` (optional) - External provider name - `bio` (optional) - User's biography - `location` (optional) - User's location +- `public_email` (optional) - The public email of the user - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `skip_confirmation` (optional) - Skip confirmation - true or false (default) @@ -305,26 +306,27 @@ PUT /users/:id Parameters: -- `email` - Email -- `username` - Username -- `name` - Name -- `password` - Password -- `skype` - Skype ID -- `linkedin` - LinkedIn -- `twitter` - Twitter account -- `website_url` - Website URL -- `organization` - Organization name -- `projects_limit` - Limit projects each user can create -- `extern_uid` - External UID -- `provider` - External provider name -- `bio` - User's biography -- `location` (optional) - User's location -- `admin` (optional) - User is admin - true or false (default) -- `can_create_group` (optional) - User can create groups - true or false +- `email` - Email +- `username` - Username +- `name` - Name +- `password` - Password +- `skype` - Skype ID +- `linkedin` - LinkedIn +- `twitter` - Twitter account +- `website_url` - Website URL +- `organization` - Organization name +- `projects_limit` - Limit projects each user can create +- `extern_uid` - External UID +- `provider` - External provider name +- `bio` - User's biography +- `location` (optional) - User's location +- `public_email` (optional) - The public email of the user +- `admin` (optional) - User is admin - true or false (default) +- `can_create_group` (optional) - User can create groups - true or false - `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default) -- `external` (optional) - Flags the user as external - true or false(default) -- `avatar` (optional) - Image file for user's avatar -- `private_profile` (optional) - User's profile is private - true or false +- `external` (optional) - Flags the user as external - true or false(default) +- `avatar` (optional) - Image file for user's avatar +- `private_profile` (optional) - User's profile is private - true or false On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md index 7990917f809..df83f30fbb7 100644 --- a/doc/ci/interactive_web_terminal/index.md +++ b/doc/ci/interactive_web_terminal/index.md @@ -17,7 +17,7 @@ Two things need to be configured for the interactive web terminal to work: - The Runner needs to have [`[session_server]` configured properly][session-server] -- Web terminals need to be +- If you are using a reverse proxy with your GitLab instance, web terminals need to be [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support) ## Debugging a running job diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 7ba8e3dce95..fed29d37b26 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -177,7 +177,7 @@ If you've decided that you would like to work on an issue, please @-mention the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what) as soon as possible. The product manager will then pull in appropriate GitLab team members to further discuss scope, design, and technical considerations. This will -ensure that that your contribution is aligned with the GitLab product and minimize +ensure that your contribution is aligned with the GitLab product and minimize any rework and delay in getting it merged into master. GitLab team members who apply the ~"Accepting Merge Requests" label to an issue @@ -226,7 +226,7 @@ code snippet right after your description in a new line: `~"feature proposal"`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. -Please submit Feature Proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Feature%20Proposal.md) provided on the issue tracker. +Please submit Feature Proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Feature%20proposal.md) provided on the issue tracker. For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md index 7290a175501..c44690a4c5d 100644 --- a/doc/development/i18n/index.md +++ b/doc/development/i18n/index.md @@ -50,3 +50,5 @@ able to proofread and instructions on becoming a proofreader yourself. ## Release Translations are typically included in the next major or minor release. + +See [Merging translations from Crowdin](merging_translations.md) diff --git a/doc/development/i18n/merging_translations.md b/doc/development/i18n/merging_translations.md new file mode 100644 index 00000000000..d172aa6da21 --- /dev/null +++ b/doc/development/i18n/merging_translations.md @@ -0,0 +1,60 @@ +# Merging translations from Crowdin + +Crowdin automatically syncs the `gitlab.pot` file presenting newly +added translations to the community of translators. + +At the same time, it creates a merge request to merge all newly added +& approved translations. Find the [merge reqeust created by +`gitlab-crowdin-bot`](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&author_username=gitlab-crowdin-bot) +to see new and merged merge requests. They are created in EE and need +to be ported to CE manually. + +## Validation + +By default Crowdin commits translations with `[skip ci]` in the commit +message. This is done to avoid a bunch of pipelines being run. Before +merging translations, make sure to trigger a pipeline to validate +translations, we have static analysis validating things Crowdin +doesn't do. Create a [new pipeline](https://gitlab.com/gitlab-org/gitlab-ee/pipelines/new) for the +`master-i18n` branch. + +If there are validation errors, the easiest solution is to disapprove +the offending string in Crowdin, leaving a comment with what is +required to fix the offense. There is an +[issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/49208) +suggesting to automate this process. Disapproving will exclude the +invalid translation, the merge request will be updated within a few +minutes. + +It might be handy to pause the integration on the Crowdin side for a +little while so translations don't keep coming. This can be done by +clicking `Pause sync` on the [Crowdin integration settings +page](https://translate.gitlab.com/project/gitlab-ee/settings#integration). + +When all failures are resolved, the translations need to be double +checked once more [as discussed in this +issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/37850). + +## Merging translations + +When all translations are found good and pipelines pass the +translations can be merged into the master branch. After that is done, +create a new merge request cherry-picking the translations from EE to +CE. When merging the translations, make sure to check the `Remove +source branch` checkbox, so Crowdin recreates the `master-i18n` from +master after the new translation was merged. + +We are discussing automating this entire process +[here](https://gitlab.com/gitlab-org/gitlab-ce/issues/39309). + +## Recreate the merge request + +Crowdin creates a new merge request as soon as the old one is closed +or merged. But it won't recreate the `master-i18n` branch every +time. To force Crowdin to recreate the branch, close any [open merge +request](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&author_username=gitlab-crowdin-bot) +and delete the +[`master-18n`](https://gitlab.com/gitlab-org/gitlab-ee/branches/all?utf8=%E2%9C%93&search=master-i18n). + +This might be needed when the merge request contains failures that +have been fixed on master. diff --git a/doc/development/performance.md b/doc/development/performance.md index 05caffb150a..c7b10dfd5ce 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -364,8 +364,7 @@ Depending on the size of the String and how frequently it would be allocated there's no guarantee it will. Strings will be frozen by default in Ruby 3.0. To prepare our code base for -this eventuality, it's a good practice to add the following header to all -Ruby files: +this eventuality, we will be adding the following header to all Ruby files: ```ruby # frozen_string_literal: true @@ -379,6 +378,9 @@ test = +"hello" test += " world" ``` +When adding new Ruby files, please check that you can add the above header, +as omitting it may lead to style check failures. + ## Anti-Patterns This is a collection of [anti-patterns][anti-pattern] that should be avoided diff --git a/doc/update/README.md b/doc/update/README.md index 2c1fbc15719..7d3c4c310a4 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -41,7 +41,8 @@ However, for this to work there are the following requirements: 1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3. 2. You have to use [post-deployment - migrations](../development/post_deployment_migrations.md). + migrations](../development/post_deployment_migrations.md) (included in + zero downtime update steps below) 3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required. diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index b55946a788f..8db36c4a0e8 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -1,9 +1,8 @@ # Subgroups -> **Notes:** -> - [Introduced][ce-2772] in GitLab 9.0. -> - Not available when using MySQL as external database (support removed in -> GitLab 9.3 [due to performance reasons][issue]). +NOTE: **Note:** +[Introduced][ce-2772] in GitLab 9.0. Not available when using MySQL as external +database (support removed in GitLab 9.3 [due to performance reasons][issue]). With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: @@ -79,14 +78,14 @@ structure. ## Creating a subgroup -> **Notes:** -> - You need to be an Owner of a group in order to be able to create -> a subgroup. For more information check the [permissions table][permissions]. -> - For a list of words that are not allowed to be used as group names see the -> [reserved names][reserved]. -> - Users can always create subgroups if they are explicitly added as an Owner to -> a parent group even if group creation is disabled by an administrator in their -> settings. +NOTE: **Note:** +You need to be an Owner of a group in order to be able to create a subgroup. For +more information check the [permissions table][permissions]. +For a list of words that are not allowed to be used as group names see the +[reserved names][reserved]. +Users can always create subgroups if they are explicitly added as an Owner to +a parent group even if group creation is disabled by an administrator in their +settings. To create a subgroup: @@ -136,12 +135,15 @@ From the image above, we can deduct the following things: ### Overriding the ancestor group membership ->**Note:** +NOTE: **Note:** You need to be an Owner of a group in order to be able to add members to it. +NOTE: **Note:** +A user's permissions in a subgroup cannot be lower than in any of its ancestor groups. +Therefore, you cannot reduce a user's permissions in a subgroup with respect to its ancestor groups. + To override a user's membership of an ancestor group (the first group they were -added to), simply add the user in the new subgroup again, but with different -permissions. +added to), add the user to the new subgroup again with a higher set of permissions. For example, if User0 was first added to group `group-1/group-1-1` with Developer permissions, then they will inherit those permissions in every other subgroup diff --git a/doc/user/project/img/issue_board_milestone_lists.png b/doc/user/project/img/issue_board_milestone_lists.png Binary files differnew file mode 100644 index 00000000000..91926f58f87 --- /dev/null +++ b/doc/user/project/img/issue_board_milestone_lists.png diff --git a/doc/user/project/img/issue_board_summed_weights.png b/doc/user/project/img/issue_board_summed_weights.png Binary files differnew file mode 100644 index 00000000000..2288d767d8c --- /dev/null +++ b/doc/user/project/img/issue_board_summed_weights.png diff --git a/doc/user/project/img/issue_boards_core.png b/doc/user/project/img/issue_boards_core.png Binary files differnew file mode 100755 index 00000000000..9e819160861 --- /dev/null +++ b/doc/user/project/img/issue_boards_core.png diff --git a/doc/user/project/img/issue_boards_premium.png b/doc/user/project/img/issue_boards_premium.png Binary files differnew file mode 100755 index 00000000000..bd9164b2961 --- /dev/null +++ b/doc/user/project/img/issue_boards_premium.png diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 5a38f5d8aed..7d12cd8f7c2 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -943,7 +943,13 @@ X-Gitlab-Event: Pipeline Hook ], "created_at": "2016-08-12 15:23:28 UTC", "finished_at": "2016-08-12 15:26:29 UTC", - "duration": 63 + "duration": 63, + "variables": [ + { + "key": "NESTOR_PROD_ENVIRONMENT", + "value": "us-west-1" + } + ] }, "user":{ "name": "Administrator", diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 7c6d547d626..464fa5987c1 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -2,13 +2,44 @@ > [Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board). +## Overview + The GitLab Issue Board is a software project management tool used to plan, organize, and visualize a workflow for a feature or product release. It can be used as a [Kanban] or a [Scrum] board. -![GitLab Issue Board](img/issue_board.png) +It provides perfect pairing between issue tracking and project management, +keeping everything in the same place, so that you don't need to jump +between different platforms to organize your workflow. -## Overview +With GitLab Issue Boards, you organize your issues in lists that correspond to +their assigned labels, visualizing issues designed as cards throughout that lists. + +You define your process and GitLab organizes it for you. You add your labels +then create the corresponding list to pull in your existing issues. When +you're ready, you can drag and drop your issue cards from one step to the next. + +![GitLab Issue Board - Core](img/issue_boards_core.png) + +**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +Watch a [video presentation](https://youtu.be/UWsJ8tkHAa8) of +Issue Boards** (version introduced in GitLab 8.11 - August 2016). + +### Advanced features of Issue Boards + +With [GitLab Starter](https://about.gitlab.com/pricing/), you can create +[multiple issue boards](#multiple-issue-boards) for a given project. **[STARTER]** + +With [GitLab Premium](https://about.gitlab.com/pricing/), you can also create multiple +issue boards for your groups, and add lists for [assignees](#assignee-lists) and +[milestones](#milestone-lists). **[PREMIUM]** + +Check all the [advanced features of Issue Boards](#gitlab-enterprise-features-for-issue-boards) +below. + +![GitLab Issue Boards - Premium](img/issue_boards_premium.png) + +## How it works The Issue Board builds on GitLab's existing [issue tracking functionality](issues/index.md#issue-tracker) and @@ -28,15 +59,12 @@ and deploy from one single platform. Issue Boards help you to visualize and manage the entire process _in_ GitLab. With [Multiple Issue Boards](#use-cases-for-multiple-issue-boards), available -only in [GitLab Enterprise Edition](#features-per-tier), +only in [different tiers of GitLab Enterprise Edition](#gitlab-enterprise-features-for-issue-boards), you go even further, as you can not only keep yourself and your project organized from a broader perspective with one Issue Board per project, but also allow your team members to organize their own workflow by creating multiple Issue Boards within the same project. -For a visual overview, see our [Issue Board feature page](https://about.gitlab.com/features/issueboard/) -on about.gitlab.com or our [video introduction to Issue Boards](https://www.youtube.com/watch?v=UWsJ8tkHAa8). - ## Use cases There are many ways to use GitLab Issue Boards tailored to your own preferred workflow. @@ -111,11 +139,6 @@ to improve their workflow with multiple boards. Create lists for each of your team members and quickly drag-and-drop issues onto each team member. -## Permissions - -[Developers and up](../permissions.md) can use all the functionality of the -Issue Board, that is, create or delete lists and drag issues from one list to another. - ## Issue Board terminology - **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards. @@ -126,6 +149,139 @@ Issue Board, that is, create or delete lists and drag issues from one list to an - **Closed** (default): shows all closed issues. Always appears as the rightmost list. - **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list. +## Permissions + +[Developers and up](../permissions.md) can use all the functionality of the +Issue Board, that is, create or delete lists and drag issues from one list to another. + +## GitLab Enterprise features for Issue Boards + +GitLab Issue Boards are available on GitLab Core and GitLab.com Free, but some +advanced functionalities are only present in higher tiers: GitLab.com Bronze, +Silver, or Gold, or GitLab self-managed Starter, Premium, and Ultimate, as described +on the following sections. + +For a collection of [features per tier](#summary-of-features-per-tier), check the summary below. + +### Multiple Issue Boards **[STARTER]** + +> Introduced in [GitLab Enterprise Edition 8.13](https://about.gitlab.com/2016/10/22/gitlab-8-13-released/#multiple-issue-boards-ee). + +Multiple Issue Boards, as the name suggests, allow for more than one Issue Board +for a given project or group. This is great for large projects with more than one team +or in situations where a repository is used to host the code of multiple +products. + +Clicking on the current board name in the upper left corner will reveal a +menu from where you can create another Issue Board and rename or delete the +existing one. + +NOTE: **Note:** +The Multiple Issue Boards feature is available for +**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**. + +![Multiple Issue Boards](img/issue_boards_multiple.png) + +### Configurable Issue Boards **[STARTER]** + +> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration). + +An Issue Board can be associated with a GitLab [Milestone](milestones/index.md#milestones), +[Labels](labels.md), Assignee and Weight +which will automatically filter the Board issues according to these fields. +This allows you to create unique boards according to your team's need. + +![Create scoped board](img/issue_board_creation.png) + +You can define the scope of your board when creating it or by clicking on the "Edit board" button. Once a milestone, assignee or weight is assigned to an Issue Board, you will no longer be able to filter +through these in the search bar. In order to do that, you need to remove the desired scope (e.g. milestone, assignee or weight) from the Issue Board. + +![Edit board configuration](img/issue_board_edit_button.png) + +If you don't have editing permission in a board, you're still able to see the configuration by clicking on "View scope". + +![Viewing board configuration](img/issue_board_view_scope.png) + +### Focus mode **[STARTER]** + +> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep). + +Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board. + +![Board focus mode](img/issue_board_focus_mode.gif) + +### Sum of Issue Weights **[STARTER]** + +The top of each list indicates the sum of issue weights for the issues that +belong to that list. This is useful when using boards for capacity allocation, +especially in combination with [assignee lists](#assignee-lists). + +![Issue Board summed weights](img/issue_board_summed_weights.png) + +### Group Issue Boards **[PREMIUM]** + +> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards). + +Accessible at the group navigation level, a group issue board offers the same features as a project-level board, +but it can display issues from all projects in that +group and its descendant subgroups. Similarly, you can only filter by group labels for these +boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only +group-level objects are available. + +NOTE: **Note:** +Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards) and +one group issue board per group was made available in GitLab 10.6 Core. + +![Group issue board](img/group_issue_board.png) + +### Assignee lists **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5784) in GitLab 11.0 Premium. + +Like a regular list that shows all issues that have the list label, you can add +an assignee list that shows all issues assigned to the given user. +You can have a board with both label lists and assignee lists. To add an +assignee list: + +1. Click **Add list**. +1. Select the **Assignee list** tab. +1. Search and click on the user you want to add as an assignee. + +Now that the assignee list is added, you can assign or unassign issues to that user +by [dragging issues](#dragging-issues-between-lists) to and/or from an assignee list. +To remove an assignee list, just as with a label list, click the trash icon. + +![Assignee lists](img/issue_board_assignee_lists.png) + +### Milestone lists **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6469) in GitLab 11.2 Premium. + +As of 11.2, you're also able to create lists of a milestone. As the name states, +these are lists that filter issues by the assigned milestone, giving you more +freedom and visibility on the Issue Board. To do so: + +1. Click **Add list**. +1. Select the **Milestone** tab. +1. Search and click on the milestone. + +Similar to the assignee lists, you're now able to [drag issues](#dragging-issues-between-lists) +to and/or from a milestone list to manipulate the milestone of the dragged issues. +As on another list types, click on the trash icon to remove it. + +![Milestone lists](img/issue_board_milestone_lists.png) + +### Summary of features per tier + +Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table: + +| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists | +|----------|--------------------------------|------------------------------|---------------------------|----------------| +| Core / Free | 1 | 1 | No | No | +| Starter / Bronze | Multiple | 1 | Yes | No | +| Premium / Silver | Multiple | Multiple | Yes | Yes | +| Ultimate / Gold | Multiple | Multiple | Yes | Yes | + ## Actions you can take on an Issue Board - [Create a new list](#creating-a-new-list). @@ -142,7 +298,7 @@ Issue Board, that is, create or delete lists and drag issues from one list to an If you are not able to perform one or more of the things above, make sure you have the right [permissions](#permissions). -## First time using the Issue Board +### First time using the Issue Board The first time you navigate to your Issue Board, you will be presented with a default list (**Done**) and a welcoming message that gives @@ -157,7 +313,7 @@ which means the system has no way of populating them automatically. That's of course if the predefined labels don't already exist. If any of them does exist, the list will be created and filled with the issues that have that label. -## Creating a new list +### Creating a new list Create a new list by clicking on the **Add list** button at the upper right corner of the Issue Board. @@ -172,7 +328,7 @@ To create a list for a label that doesn't yet exist, simply create the label by choosing **Create new label**. The label will be created on-the-fly and it will be immediately added to the dropdown. You can now choose it to create a list. -## Deleting a list +### Deleting a list To delete a list from the Issue Board use the small trash icon that is present in the list's heading. A confirmation dialog will appear for you to confirm. @@ -180,7 +336,7 @@ in the list's heading. A confirmation dialog will appear for you to confirm. Deleting a list doesn't have any effect in issues and labels, it's just the list view that is removed. You can always add it back later if you need. -## Adding issues to a list +### Adding issues to a list You can add issues to a list by clicking the **Add issues** button that is present in the upper right corner of the Issue Board. This will open up a modal @@ -192,7 +348,7 @@ the list by filtering by author, assignee, milestone and label. ![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png) -## Removing an issue from a list +### Removing an issue from a list Removing an issue from a list can be done by clicking on the issue card and then clicking the **Remove from board** button in the sidebar. Under the hood, the @@ -201,7 +357,7 @@ board itself. ![Remove issue from list](img/issue_boards_remove_issue.png) -## Issue ordering in a list +### Issue ordering in a list When visiting a board, issues appear ordered in any list. You are able to change that order simply by dragging and dropping the issues. The changed order will be saved @@ -224,7 +380,7 @@ a given board inside your GitLab instance, any time those two issues are subsequ loaded in any board in the same instance (could be a different project board or a different group board, for example), that ordering will be maintained. -## Filtering issues +### Filtering issues You should be able to use the filters on top of your Issue Board to show only the results you want. This is similar to the filtering used in the issue tracker @@ -232,7 +388,7 @@ since the metadata from the issues and labels are re-used in the Issue Board. You can filter by author, assignee, milestone and label. -## Creating workflows +### Creating workflows By reordering your lists, you can create workflows. As lists in Issue Boards are based on labels, it works out of the box with your existing issues. So if you've @@ -267,89 +423,7 @@ to another list the label changes and a system not is recorded. ![Issue Board system notes](img/issue_board_system_notes.png) -## Multiple Issue Boards **[STARTER]** - -> Introduced in [GitLab Enterprise Edition 8.13](https://about.gitlab.com/2016/10/22/gitlab-8-13-released/#multiple-issue-boards-ee). - -Multiple Issue Boards, as the name suggests, allow for more than one Issue Board -for a given project or group. This is great for large projects with more than one team -or in situations where a repository is used to host the code of multiple -products. - -Clicking on the current board name in the upper left corner will reveal a -menu from where you can create another Issue Board and rename or delete the -existing one. - -NOTE: **Note:** -The Multiple Issue Boards feature is available for -**projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**. - -![Multiple Issue Boards](img/issue_boards_multiple.png) - -## Configurable Issue Boards **[STARTER]** - -> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration). - -An Issue Board can be associated with GitLab [Milestone](milestones/index.md#milestones), -[Labels](labels.md), Assignee and Weight -which will automatically filter the Board issues according to these fields. -This allows you to create unique boards according to your team's need. - -![Create scoped board](img/issue_board_creation.png) - -You can define the scope of your board when creating it or by clicking on the "Edit board" button. Once a milestone, assignee or weight is assigned to an Issue Board, you will no longer be able to filter -through these in the search bar. In order to do that, you need to remove the desired scope (e.g. milestone, assignee or weight) from the Issue Board. - -![Edit board configuration](img/issue_board_edit_button.png) - -If you don't have editing permission in a board, you're still able to see the configuration by clicking on "View scope". - -![Viewing board configuration](img/issue_board_view_scope.png) - -## Focus mode **[STARTER]** - -> Introduced in [GitLab Starter 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep). - -Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board. - -![Board focus mode](img/issue_board_focus_mode.gif) - -## Group Issue Boards **[PREMIUM]** - -> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards). - -Accessible at the group navigation level, a group issue board offers the same features as a project-level board, -but it can display issues from all projects in that -group and its descendant subgroups. Similarly, you can only filter by group labels for these -boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only -group-level objects are available. - -NOTE: **Note:** -Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards) and -one group issue board per group was made available in GitLab 10.6 Core. - -![Group issue board](img/group_issue_board.png) - -## Assignee lists **[PREMIUM]** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5784) in GitLab 11.0 Premium. - -Like a regular list that shows all issues that have the list label, you can add -an assignee list that shows all issues assigned to the given user. -You can have a board with both label lists and assignee lists. To add an -assignee list: - -1. Click **Add list**. -1. Select the **Assignee list** tab. -1. Search and click on the user you want to add as an assignee. - -Now that the assignee list is added, you can assign or unassign issues to that user -by [dragging issues](#dragging-issues-between-lists) to and/or from an assignee list. -To remove an assignee list, just as with a label list, click the trash icon. - -![Assignee lists](img/issue_board_assignee_lists.png) - -## Dragging issues between lists +### Dragging issues between lists When dragging issues between lists, different behavior occurs depending on the source list and the target list. @@ -360,17 +434,6 @@ When dragging issues between lists, different behavior occurs depending on the s | From label `A` list | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned | | From assignee `Alice` list | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned | -## Features per tier - -Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table: - -| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists | -|----------|--------------------------------|------------------------------|---------------------------|----------------| -| Core | 1 | 1 | No | No | -| Starter | Multiple | 1 | Yes | No | -| Premium | Multiple | Multiple | Yes | Yes | -| Ultimate | Multiple | Multiple | Yes | Yes | - ## Tips A few things to remember: diff --git a/doc/user/project/pages/img/icons/click.png b/doc/user/project/pages/img/icons/click.png Binary files differnew file mode 100644 index 00000000000..daaf760ec08 --- /dev/null +++ b/doc/user/project/pages/img/icons/click.png diff --git a/doc/user/project/pages/img/icons/cogs.png b/doc/user/project/pages/img/icons/cogs.png Binary files differnew file mode 100644 index 00000000000..a12da1b5e8c --- /dev/null +++ b/doc/user/project/pages/img/icons/cogs.png diff --git a/doc/user/project/pages/img/icons/fork.png b/doc/user/project/pages/img/icons/fork.png Binary files differnew file mode 100644 index 00000000000..e2c9577e7ce --- /dev/null +++ b/doc/user/project/pages/img/icons/fork.png diff --git a/doc/user/project/pages/img/icons/free.png b/doc/user/project/pages/img/icons/free.png Binary files differnew file mode 100644 index 00000000000..3b8f8f6863e --- /dev/null +++ b/doc/user/project/pages/img/icons/free.png diff --git a/doc/user/project/pages/img/icons/lock.png b/doc/user/project/pages/img/icons/lock.png Binary files differnew file mode 100644 index 00000000000..1c1f0b4457b --- /dev/null +++ b/doc/user/project/pages/img/icons/lock.png diff --git a/doc/user/project/pages/img/icons/monitor.png b/doc/user/project/pages/img/icons/monitor.png Binary files differnew file mode 100644 index 00000000000..7b99d430eef --- /dev/null +++ b/doc/user/project/pages/img/icons/monitor.png diff --git a/doc/user/project/pages/img/icons/terminal.png b/doc/user/project/pages/img/icons/terminal.png Binary files differnew file mode 100644 index 00000000000..ab5ae11310c --- /dev/null +++ b/doc/user/project/pages/img/icons/terminal.png diff --git a/doc/user/project/pages/img/ssgs_pages.png b/doc/user/project/pages/img/ssgs_pages.png Binary files differnew file mode 100644 index 00000000000..608881c8e31 --- /dev/null +++ b/doc/user/project/pages/img/ssgs_pages.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 4f0774dba5c..60144fa1971 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -4,71 +4,180 @@ description: 'Learn how to use GitLab Pages to deploy a static website at no add # GitLab Pages -With GitLab Pages it's easy to publish your project website. GitLab Pages is a hosting service for static websites, at no additional cost. - -## Getting Started - -[Create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch) -to get you started quickly, or, -alternatively, start from an existing project as follows: - -1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages): +**GitLab Pages is a feature that allows you to publish static websites +directly from a repository in GitLab.** + +You can use it either for personal or business websites, such as +portfolios, documentation, manifestos, and business presentations, +and attribute any license to your content. + +<table class="borderless-table center fixed-table"> + <tr> + <td style="width: 22%"><img src="img/icons/cogs.png" alt="SSGs" class="image-noshadow half-width"></td> + <td style="width: 4%"> + <strong> + <i class="fa fa-angle-double-right" aria-hidden="true"></i> + </strong> + </td> + <td style="width: 22%"><img src="img/icons/monitor.png" alt="Websites" class="image-noshadow half-width"></td> + <td style="width: 4%"> + <strong> + <i class="fa fa-angle-double-right" aria-hidden="true"></i> + </strong> + </td> + <td style="width: 22%"><img src="img/icons/free.png" alt="Pages is free" class="image-noshadow half-width"></td> + <td style="width: 4%"> + <strong> + <i class="fa fa-angle-double-right" aria-hidden="true"></i> + </strong> + </td> + <td style="width: 22%"><img src="img/icons/lock.png" alt="Secure your website" class="image-noshadow half-width"></td> + </tr> + <tr> + <td><em>Use any static website generator or plain HTML</em></td> + <td></td> + <td><em>Create websites for your projects, groups, or user account</em></td> + <td></td> + <td><em>Host on GitLab.com for free, or on your own GitLab instance</em></td> + <td></td> + <td><em>Connect your custom domain(s) and TLS certificates</em></td> + </tr> +</table> + +Pages is available for free for all GitLab.com users as well as for self-managed +instances (GitLab Core, Starter, Premium, and Ultimate). + +## Overview + +<div class="row"> +<div class="col-md-9"> +<p style="margin-top: 18px;"> +To publish a website with Pages, you can use any Static Site Generator (SSG), +such as Jekyll, Hugo, Middleman, Harp, Hexo, and Brunch, just to name a few. You can also +publish any website written directly in plain HTML, CSS, and JavaScript.</p> +<p>Pages does <strong>not</strong> support dynamic server-side processing, for instance, as <code>.php</code> and <code>.asp</code> requires. See this article to learn more about +<a href="https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/">static websites vs dynamic websites</a>.</p> +</div> +<div class="col-md-3"><img src="img/ssgs_pages.png" alt="Examples of SSGs supported by Pages" class="image-noshadow middle display-block"></div> +</div> + +### Availability + +If you're using GitLab.com, your website will be publicly available to the internet. +If you're using self-managed instances (Core, Starter, Premium, or Ultimate), +your websites will be published on your own server, according to the +[Pages admin settings](../../../administration/pages/index.md) chosen by your sysdamin, +who can opt for making them public or internal to your server. + +### How it works + +To use GitLab Pages, first you need to create a project in GitLab to upload your website's +files to. These projects can be either public, internal, or private, at your own choice. +GitLab will always deploy your website from a very specific folder called `public` in your +repository. Note that when you create a new project in GitLab, a [repository](../repository/index.md) +becomes available automatically. + +To deploy your site, GitLab will use its built-in tool called [GitLab CI/CD](../../../ci/README.md), +that will build your site and publish it to the GitLab Pages server. The sequence of +scripts that GitLab CI/CD runs to accomplish this task is created from a file named +`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will. + +You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-domain), +`*.gitlab.io`, or your own domain (`example.com`). In that case, you'll +need admin access to your domain's registrar (or control panel) to set it up with Pages. + +Optionally, when adding your own domain, you can add an SSL/TLS certificate to secure your +site under the HTTPS protocol. + +## Getting started + +To get started with GitLab Pages, you can either [create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch) +or quickly start from copying an existing example project, as follows: + +1. Choose an [example project](https://gitlab.com/pages) to [fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project): by forking a project, you create a copy of the codebase you're forking from to start from a template instead of starting from scratch. -2. Change a file to trigger a GitLab CI/CD pipeline: GitLab CI/CD will build and deploy your site to GitLab Pages. -3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live! :) - - _Further steps (optional):_ - -4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) +1. From the left sidebar, navigate to your project's **CI/CD > Pipelines** and click +**Run pipeline** so that GitLab CI/CD will build and deploy your site to the server. +1. Once the pipeline has finished successfully, find the link to visit your website from your + project's **Settings > Pages**. + +<table class="borderless-table center fixed-table middle width-80"> + <tr> + <td style="width: 30%"><img src="img/icons/fork.png" alt="Fork" class="image-noshadow half-width"></td> + <td style="width: 10%"> + <strong> + <i class="fa fa-angle-double-right" aria-hidden="true"></i> + </strong> + </td> + <td style="width: 30%"><img src="img/icons/terminal.png" alt="Deploy" class="image-noshadow half-width"></td> + <td style="width: 10%"> + <strong> + <i class="fa fa-angle-double-right" aria-hidden="true"></i> + </strong> + </td> + <td style="width: 30%"><img src="img/icons/click.png" alt="Visit" class="image-noshadow half-width"></td> + </tr> + <tr> + <td><em>Fork an example project</em></td> + <td></td> + <td><em>Deploy your website</em></td> + <td></td> + <td><em>Visit your website's URL</em></td> + </tr> +</table> + +Your website is then visible on your domain, and you can modify your files +as you wish. For every modification pushed to your repository, GitLab CI/CD will run +a new pipeline to publish your changes to the server. + +You can also take some optional further steps: + +- Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) (_You don't need the relationship unless you intent to contribute back to the example project you forked from_). -5. Make it a [user/group website](getting_started_part_one.md#user-and-group-websites) +- Make it a [user/group website](getting_started_part_one.md#user-and-group-websites) -**Watch a video with the steps above: https://www.youtube.com/watch?v=TWqh9MtT4Bg** +**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=TWqh9MtT4Bg) with all the steps above!** _Advanced options:_ - [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages) - Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain -## How Does It Work? - -With GitLab Pages you can create [static websites](getting_started_part_one.md#what-you-need-to-know-before-getting-started) -for your GitLab projects, groups, or user accounts. - -It supports plain static content, such as HTML, and **all** [static site generators (SSGs)](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/), such as Jekyll, Middleman, Hexo, Hugo, and Pelican. - -Connect as many custom domains as you like and bring your own TLS certificate -to secure them. - -Your files live in a project [repository](../repository/index.md) on GitLab. -[GitLab CI](../../../ci/README.md) picks up those files and makes them available at, typically, -`https://<username>.gitlab.io/<projectname>`. Please read through the docs on -[GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain) for more info. - ## Explore GitLab Pages -Read the following tutorials to know more about: +To learn more about GitLab Pages, read the following tutorials: - [Static websites and GitLab Pages domains](getting_started_part_one.md): Understand what is a static website, and how GitLab Pages default domains work - [Projects for GitLab Pages and URL structure](getting_started_part_two.md): Forking projects and creating new ones from scratch, understanding URLs structure and baseurls -- [GitLab Pages custom domains and SSL/TLS Certificates](getting_started_part_three.md): How to add custom domains and subdomains to your website, configure DNS records, and SSL/TLS certificates +- [GitLab Pages custom domains and SSL/TLS Certificates](getting_started_part_three.md): How to add custom domains and subdomains to your website, configure DNS records and SSL/TLS certificates - [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md): Understand how to create your own `.gitlab-ci.yml` for your site - [Technical aspects, custom 404 pages, limitations](introduction.md) -- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) (outdated) -_Blog posts series about Static Site Generators (SSGs):_ +### GitLab Pages with Static Site Generators (SSGs) + +To understand more about SSGs, their advantages, and how to get the most from them +with Pages, read through this series: - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -_Blog posts for securing GitLab Pages custom domains with SSL/TLS certificates:_ +### GitLab Pages with SSL/TLS certificates + +If you're using GitLab Pages default domain (`.gitlab.io`), your website will be +automatically secure and available under HTTPS. If you're using your own domain, you can +optionally secure it with with SSL/TLS certificates. You can read the following +tutorials to learn how to use these third-party certificates with GitLab Pages: - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) -- [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) (outdated) +- [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) (mind that although this article is out-of-date, it can still be useful to guide you through the basic steps) ## Advanced use +There are quite some great examples of GitLab Pages websites built for some +specific reasons. These examples can teach you some advanced techniques +to use and adapt to your own needs: + - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) @@ -80,10 +189,9 @@ _Blog posts for securing GitLab Pages custom domains with SSL/TLS certificates:_ Enable and configure GitLab Pages on your own instance (GitLab Community Edition and Enterprise Editions) with the [admin guide](../../../administration/pages/index.md). -**Watch the video: https://www.youtube.com/watch?v=dD8c7WNcc6s** +**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=dD8c7WNcc6s) for getting started with GitLab Pages admin!** ## More information about GitLab Pages -- For an overview, visit the [feature webpage](https://about.gitlab.com/features/pages/) - Announcement (2016-12-24): ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) - Announcement (2017-03-06): ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 18063fb20a2..cecff6d3b81 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class AccessRequests < Grape::API include PaginationParams diff --git a/lib/api/api.rb b/lib/api/api.rb index e89d9337853..06c8b48b8cc 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class API < Grape::API include APIGuard diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 8ee7987cfff..61357b3f1d6 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Guard API with OAuth 2.0 Access Token require 'rack/oauth2' diff --git a/lib/api/applications.rb b/lib/api/applications.rb index b122cdefe4e..f29cd7fc003 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # External applications API class Applications < Grape::API diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb index 70219bc8ea0..0f14d003065 100644 --- a/lib/api/avatar.rb +++ b/lib/api/avatar.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Avatar < Grape::API resource :avatar do diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index e334af22183..c2abf9155f3 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class AwardEmoji < Grape::API include PaginationParams diff --git a/lib/api/badges.rb b/lib/api/badges.rb index 8ceffe9c5ef..ab670988f47 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Badges < Grape::API include PaginationParams diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 0f89414148b..c80e1c57864 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Boards < Grape::API include BoardsResponses diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb index 3322b37c6ff..86d9b24802f 100644 --- a/lib/api/boards_responses.rb +++ b/lib/api/boards_responses.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module BoardsResponses extend ActiveSupport::Concern diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 5d106ed93a0..2735d410c8e 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'mime/types' module API diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index d7138b2f2fe..19148758fc5 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class BroadcastMessages < Grape::API include PaginationParams diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb index c13154dc0ec..6eddc5e5b61 100644 --- a/lib/api/circuit_breakers.rb +++ b/lib/api/circuit_breakers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class CircuitBreakers < Grape::API before { authenticated_as_admin! } diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 8e6f706afd4..99553d993ca 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'mime/types' module API diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 5aeffc8fb99..f0db1318146 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'mime/types' module API diff --git a/lib/api/custom_attributes_endpoints.rb b/lib/api/custom_attributes_endpoints.rb index b5864665cc3..2149e04451e 100644 --- a/lib/api/custom_attributes_endpoints.rb +++ b/lib/api/custom_attributes_endpoints.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module CustomAttributesEndpoints extend ActiveSupport::Concern diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 501e9f64db0..ce35720d408 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class DeployKeys < Grape::API include PaginationParams diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index b7892599295..6747e2e5005 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Deployments RESTful API endpoints class Deployments < Grape::API diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 88668992215..39c6d28391d 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Discussions < Grape::API include PaginationParams diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 12c4340c1ba..a78a93cbfd9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Entities class WikiPageBasic < Grape::Entity diff --git a/lib/api/environments.rb b/lib/api/environments.rb index fa828f43001..c64217a6977 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Environments RESTfull API endpoints class Environments < Grape::API diff --git a/lib/api/events.rb b/lib/api/events.rb index dfe0e81af26..6e0b508be19 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Events < Grape::API include PaginationParams @@ -16,12 +18,27 @@ module API desc: 'Return events sorted in ascending and descending order' end + RedactedEvent = OpenStruct.new(target_title: 'Confidential event').freeze + + def redact_events(events) + events.map do |event| + if event.visible_to_user?(current_user) + event + else + RedactedEvent + end + end + end + # rubocop: disable CodeReuse/ActiveRecord - def present_events(events) + def present_events(events, redact: true) events = events.reorder(created_at: params[:sort]) .with_associations - present paginate(events), with: Entities::Event + events = paginate(events) + events = redact_events(events) if redact + + present events, with: Entities::Event end # rubocop: enable CodeReuse/ActiveRecord end @@ -44,7 +61,8 @@ module API events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target) - present_events(events) + # Since we're viewing our own events, redaction is unnecessary + present_events(events, redact: false) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/api/features.rb b/lib/api/features.rb index 79be8c1903e..6f2422af13a 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Features < Grape::API before { authenticated_as_admin! } diff --git a/lib/api/files.rb b/lib/api/files.rb index ac02488d30c..bcd2cd48a45 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Files < Grape::API FILE_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index 3832cdc10a8..dc30e868e2e 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class GroupBoards < Grape::API include BoardsResponses diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb index 4b4352c2b27..b36436dbf43 100644 --- a/lib/api/group_milestones.rb +++ b/lib/api/group_milestones.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class GroupMilestones < Grape::API include MilestoneResponses diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index b6610dd04b3..ae7241e9a30 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class GroupVariables < Grape::API include PaginationParams diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 018ca72c32a..64b998ab455 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Groups < Grape::API include PaginationParams diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 85e3e06e4fd..a7ba8066233 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers include Gitlab::Utils @@ -381,9 +383,10 @@ module API # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 trace = exception.backtrace - message = "\n#{exception.class} (#{exception.message}):\n" + message = ["\n#{exception.class} (#{exception.message}):\n"] message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << trace.join("\n ") + message = message.join API.logger.add Logger::FATAL, message diff --git a/lib/api/helpers/badges_helpers.rb b/lib/api/helpers/badges_helpers.rb index 1f8afbf3c90..46ce5b4e7b5 100644 --- a/lib/api/helpers/badges_helpers.rb +++ b/lib/api/helpers/badges_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module BadgesHelpers diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb index 9993caa5249..7551ca50a7f 100644 --- a/lib/api/helpers/common_helpers.rb +++ b/lib/api/helpers/common_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module CommonHelpers diff --git a/lib/api/helpers/custom_attributes.rb b/lib/api/helpers/custom_attributes.rb index 3bbe827967e..88208226c40 100644 --- a/lib/api/helpers/custom_attributes.rb +++ b/lib/api/helpers/custom_attributes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module CustomAttributes diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index dd4f6c41131..23b1cd1ad45 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module CustomValidators diff --git a/lib/api/helpers/headers_helpers.rb b/lib/api/helpers/headers_helpers.rb index c9c44e3c218..7553af9d156 100644 --- a/lib/api/helpers/headers_helpers.rb +++ b/lib/api/helpers/headers_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module HeadersHelpers diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 83151be82ad..4eaaca96b49 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module InternalHelpers diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 518aaa62aef..73d58ee7f37 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # rubocop:disable GitlabSecurity/PublicSend module API diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 7b1f5c2584b..216b2c45741 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module NotesHelpers diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index 50bcd4e0437..d311cbb5f7e 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module Pagination diff --git a/lib/api/helpers/project_snapshots_helpers.rb b/lib/api/helpers/project_snapshots_helpers.rb index 94798a8cb51..1b5dc281e38 100644 --- a/lib/api/helpers/project_snapshots_helpers.rb +++ b/lib/api/helpers/project_snapshots_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module ProjectSnapshotsHelpers diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 98672f2f765..e6a72b949f9 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module ProjectsHelpers diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb index bc7333ca4b3..793ae11b41d 100644 --- a/lib/api/helpers/related_resources_helpers.rb +++ b/lib/api/helpers/related_resources_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module RelatedResourcesHelpers diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 61eb88d3331..45d0343bc89 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module Helpers module Runner diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 71b87f60bf6..6a264c4cc6d 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Internal access API class Internal < Grape::API diff --git a/lib/api/issues.rb b/lib/api/issues.rb index bcb03a0b540..25d78053c88 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Issues < Grape::API include PaginationParams diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index ab4203c4e25..2229cbcd9d4 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class JobArtifacts < Grape::API before { authenticate_non_get! } diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 27ffd42834c..63fab6b0abb 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Jobs < Grape::API include PaginationParams diff --git a/lib/api/keys.rb b/lib/api/keys.rb index fd93f797f72..d5280a0035d 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Keys API class Keys < Grape::API diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 98c9818db39..28555454307 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Labels < Grape::API include PaginationParams diff --git a/lib/api/lint.rb b/lib/api/lint.rb index d202eaa4c49..0342a4b6654 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Lint < Grape::API namespace :ci do diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb index 5d55224c1a7..50d8a1ac596 100644 --- a/lib/api/markdown.rb +++ b/lib/api/markdown.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Markdown < Grape::API params do diff --git a/lib/api/members.rb b/lib/api/members.rb index 4d8e23dee91..a8f67be3463 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Members < Grape::API include PaginationParams diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 95ef8f42954..e4fb890960a 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # MergeRequestDiff API class MergeRequestDiffs < Grape::API diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index cad38271cbb..764905ca00f 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class MergeRequests < Grape::API include PaginationParams diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index a8eb137e46a..a0ca39b69d4 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module MilestoneResponses extend ActiveSupport::Concern diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 32b77aedba8..76639fbb031 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Namespaces < Grape::API include PaginationParams diff --git a/lib/api/notes.rb b/lib/api/notes.rb index dc9373bb3c2..9f323b87baf 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Notes < Grape::API include PaginationParams diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index bf0d6b9e434..4d9a4629268 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # notification_settings API class NotificationSettings < Grape::API diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 8730c91b426..c9ad47e0f0d 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class PagesDomains < Grape::API include PaginationParams diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb index f566eb3ed2b..ae03595eb25 100644 --- a/lib/api/pagination_params.rb +++ b/lib/api/pagination_params.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Concern for declare pagination params. # diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index 5bd1ce8c5e1..ed0a38b9d70 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class PipelineSchedules < Grape::API include PaginationParams diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 5cce96d5ae7..1cfb982c04b 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Pipelines < Grape::API include PaginationParams diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 8562ae6d737..e34ed0bdb44 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectExport < Grape::API before do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 0fb454bc22e..4af4c6ac593 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectHooks < Grape::API include PaginationParams diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index bc5152e539f..cbfa0c5bc1c 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectImport < Grape::API include PaginationParams diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index 72cf32d7717..c7137ba5217 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectMilestones < Grape::API include PaginationParams diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb index 71005acc587..175fbb2ce92 100644 --- a/lib/api/project_snapshots.rb +++ b/lib/api/project_snapshots.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectSnapshots < Grape::API helpers ::API::Helpers::ProjectSnapshotsHelpers diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 1ef176b1320..f3a1b73b153 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProjectSnippets < Grape::API include PaginationParams diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ee426f39523..00bad49ebdc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_dependency 'declarative_policy' module API diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb index 9fd79c491c2..8edcfea7c93 100644 --- a/lib/api/projects_relation_builder.rb +++ b/lib/api/projects_relation_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module ProjectsRelationBuilder extend ActiveSupport::Concern diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 804f6fa9b73..47752f40e58 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProtectedBranches < Grape::API include PaginationParams diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb index e406344e42d..ed1c5f0cc05 100644 --- a/lib/api/protected_tags.rb +++ b/lib/api/protected_tags.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class ProtectedTags < Grape::API include PaginationParams diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 79736107bbb..5125f302fbb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'mime/types' module API diff --git a/lib/api/runner.rb b/lib/api/runner.rb index b2d46cef23c..d8768a54986 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Runner < Grape::API helpers ::API::Helpers::Runner diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 9bcdfc8cb15..60868821810 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Runners < Grape::API include PaginationParams diff --git a/lib/api/scope.rb b/lib/api/scope.rb index d5165b2e482..707775e5d15 100644 --- a/lib/api/scope.rb +++ b/lib/api/scope.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Encapsulate a scope used for authorization, such as `api`, or `read_user` module API class Scope diff --git a/lib/api/search.rb b/lib/api/search.rb index 37fbabe419c..12d97dcfe7f 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Search < Grape::API include PaginationParams diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 8d71bd9dff1..edbd134822c 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Settings < Grape::API before { authenticated_as_admin! } diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index 11f2b40269a..daa9598a204 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'sidekiq/api' module API diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 6352a9c8742..f1786c15f4f 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API # Snippets API class Snippets < Grape::API diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index b3e1e23031a..077e9373ac4 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Subscriptions < Grape::API before { authenticate! } diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 07552aa18e8..51fae0e54aa 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class SystemHooks < Grape::API include PaginationParams diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 5e0afc6a7e4..f739eacf9ba 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Tags < Grape::API include PaginationParams diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 7bf0e0f5934..8ff3b2ac33c 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Templates < Grape::API include PaginationParams diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb index 2bb451dea89..93fe06bec27 100644 --- a/lib/api/time_tracking_endpoints.rb +++ b/lib/api/time_tracking_endpoints.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API module TimeTrackingEndpoints extend ActiveSupport::Concern diff --git a/lib/api/todos.rb b/lib/api/todos.rb index c6dbcf84e3a..ed2cf2cc31b 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Todos < Grape::API include PaginationParams diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 2339505b05b..f784c857883 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Triggers < Grape::API include PaginationParams diff --git a/lib/api/users.rb b/lib/api/users.rb index ac09ca7f7b7..11a7f4ef64d 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Users < Grape::API include PaginationParams @@ -42,12 +44,12 @@ module API optional :provider, type: String, desc: 'The external provider' optional :bio, type: String, desc: 'The biography of the user' optional :location, type: String, desc: 'The location of the user' + optional :public_email, type: String, desc: 'The public email of the user' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' optional :avatar, type: File, desc: 'Avatar image for user' optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' - optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user' all_or_none_of :extern_uid, :provider end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 50e6fa6bcdf..c844ba321ed 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Variables < Grape::API include PaginationParams diff --git a/lib/api/version.rb b/lib/api/version.rb index 3b10bfa6a7d..74cd857f447 100644 --- a/lib/api/version.rb +++ b/lib/api/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Version < Grape::API before { authenticate! } diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index e86ebc573f2..6e1d4eb335f 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module API class Wikis < Grape::API helpers do diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb index 45a935ab352..33658ae225f 100644 --- a/lib/backup/artifacts.rb +++ b/lib/backup/artifacts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb index adf85ca4719..5e795a449de 100644 --- a/lib/backup/builds.rb +++ b/lib/backup/builds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 086ca5986bd..e6bf3d1856f 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'yaml' module Backup diff --git a/lib/backup/files.rb b/lib/backup/files.rb index e287aa1e392..0032ae8f84b 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'open3' require_relative 'helper' diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb index 54b9ce10b4d..22f00aef569 100644 --- a/lib/backup/helper.rb +++ b/lib/backup/helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Backup module Helper def access_denied_error(path) diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb index 185ff8ae6bd..0dfe56e214f 100644 --- a/lib/backup/lfs.rb +++ b/lib/backup/lfs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index a3641505196..5d4a7efc456 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Backup class Manager ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb index 542e35a7c7c..a4be728df08 100644 --- a/lib/backup/pages.rb +++ b/lib/backup/pages.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb index 35821805797..d16ed2facf1 100644 --- a/lib/backup/registry.rb +++ b/lib/backup/registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 906ed498026..c8a5377bfa0 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'yaml' module Backup diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 49b117a7ee3..9577df2634a 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'backup/files' module Backup diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 8e838d04bad..7acbc933adc 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -60,7 +60,11 @@ module Banzai path_parts.unshift(relative_url_root, project.full_path) end - path = Addressable::URI.escape(File.join(*path_parts)) + begin + path = Addressable::URI.escape(File.join(*path_parts)) + rescue Addressable::URI::InvalidURIError + return + end html_attr.value = if context[:only_path] diff --git a/lib/gitlab/background_migration/encrypt_columns.rb b/lib/gitlab/background_migration/encrypt_columns.rb new file mode 100644 index 00000000000..0d333e47e7b --- /dev/null +++ b/lib/gitlab/background_migration/encrypt_columns.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # EncryptColumn migrates data from an unencrypted column - `foo`, say - to + # an encrypted column - `encrypted_foo`, say. + # + # For this background migration to work, the table that is migrated _has_ to + # have an `id` column as the primary key. Additionally, the encrypted column + # should be managed by attr_encrypted, and map to an attribute with the same + # name as the unencrypted column (i.e., the unencrypted column should be + # shadowed). + # + # To avoid depending on a particular version of the model in app/, add a + # model to `lib/gitlab/background_migration/models/encrypt_columns` and use + # it in the migration that enqueues the jobs, so code can be shared. + class EncryptColumns + def perform(model, attributes, from, to) + model = model.constantize if model.is_a?(String) + attributes = expand_attributes(model, Array(attributes).map(&:to_sym)) + + model.transaction do + # Use SELECT ... FOR UPDATE to prevent the value being changed while + # we are encrypting it + relation = model.where(id: from..to).lock + + relation.each do |instance| + encrypt!(instance, attributes) + end + end + end + + private + + # Build a hash of { attribute => encrypted column name } + def expand_attributes(klass, attributes) + expanded = attributes.flat_map do |attribute| + attr_config = klass.encrypted_attributes[attribute] + crypt_column_name = attr_config&.fetch(:attribute) + + raise "Couldn't determine encrypted column for #{klass}##{attribute}" if + crypt_column_name.nil? + + [attribute, crypt_column_name] + end + + Hash[*expanded] + end + + # Generate ciphertext for each column and update the database + def encrypt!(instance, attributes) + to_clear = attributes + .map { |plain, crypt| apply_attribute!(instance, plain, crypt) } + .compact + .flat_map { |plain| [plain, nil] } + + to_clear = Hash[*to_clear] + + if instance.changed? + instance.save! + instance.update_columns(to_clear) + end + end + + def apply_attribute!(instance, plain_column, crypt_column) + plaintext = instance[plain_column] + ciphertext = instance[crypt_column] + + # No need to do anything if the plaintext is nil, or an encrypted + # value already exists + return nil unless plaintext.present? && !ciphertext.present? + + # attr_encrypted will calculate and set the expected value for us + instance.public_send("#{plain_column}=", plaintext) # rubocop:disable GitlabSecurity/PublicSend + + plain_column + end + end + end +end diff --git a/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb b/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb new file mode 100644 index 00000000000..bb76eb8ed48 --- /dev/null +++ b/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module Models + module EncryptColumns + # This model is shared between synchronous and background migrations to + # encrypt the `token` and `url` columns + class WebHook < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'web_hooks' + self.inheritance_column = :_type_disabled + + attr_encrypted :token, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + + attr_encrypted :url, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + end + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_external_pipeline_source.rb b/lib/gitlab/background_migration/populate_external_pipeline_source.rb new file mode 100644 index 00000000000..036fe641757 --- /dev/null +++ b/lib/gitlab/background_migration/populate_external_pipeline_source.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class PopulateExternalPipelineSource + module Migratable + class Pipeline < ActiveRecord::Base + self.table_name = 'ci_pipelines' + + def self.sources + { + unknown: nil, + push: 1, + web: 2, + trigger: 3, + schedule: 4, + api: 5, + external: 6 + } + end + end + + class CommitStatus < ActiveRecord::Base + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled + + scope :has_pipeline, -> { where('ci_builds.commit_id=ci_pipelines.id') } + scope :of_type, -> (type) { where('type=?', type) } + end + end + + def perform(start_id, stop_id) + external_pipelines(start_id, stop_id) + .update_all(source: Migratable::Pipeline.sources[:external]) + end + + private + + def external_pipelines(start_id, stop_id) + Migratable::Pipeline.where(id: (start_id..stop_id)) + .where( + 'EXISTS (?) AND NOT EXISTS (?)', + Migratable::CommitStatus.of_type('GenericCommitStatus').has_pipeline.select(1), + Migratable::CommitStatus.of_type('Ci::Build').has_pipeline.select(1) + ) + end + end + end +end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index d8fcdfac266..aebaf2d6982 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -162,7 +162,10 @@ sast: artifacts: paths: [gl-sast-report.json] only: - - branches + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bsast\b/ except: variables: - $SAST_DISABLED @@ -179,7 +182,10 @@ dependency_scanning: artifacts: paths: [gl-dependency-scanning-report.json] only: - - branches + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ except: variables: - $DEPENDENCY_SCANNING_DISABLED @@ -196,7 +202,10 @@ container_scanning: artifacts: paths: [gl-container-scanning-report.json] only: - - branches + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\bsast_container\b/ except: variables: - $CONTAINER_SCANNING_DISABLED @@ -215,6 +224,8 @@ dast: refs: - branches kubernetes: active + variables: + - $GITLAB_FEATURES =~ /\bdast\b/ except: refs: - master diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index eb246d393a1..f382992cb0a 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -26,7 +26,8 @@ module Gitlab stages: pipeline.stages_names, created_at: pipeline.created_at, finished_at: pipeline.finished_at, - duration: pipeline.duration + duration: pipeline.duration, + variables: pipeline.variables.map(&:hook_attrs) } end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 7f012312819..30541ee3553 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1073,6 +1073,10 @@ into similar problems in the future (e.g. when new tables are created). connection.select_value(index_sql).to_i > 0 end + + def mysql_compatible_index_length + Gitlab::Database.mysql? ? 20 : nil + end end end end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 1f012043e56..a605ddb5c33 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -24,7 +24,7 @@ module Gitlab # ignore highlighting for "match" lines next diff_line if diff_line.meta? - rich_line = highlight_line(diff_line) || diff_line.text + rich_line = highlight_line(diff_line) || ERB::Util.html_escape(diff_line.text) if line_inline_diffs = inline_diffs[i] begin diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 3d5a63bdbac..45d42c7078f 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -953,6 +953,12 @@ module Gitlab end end + def list_last_commits_for_tree(sha, path, offset: 0, limit: 25) + wrapped_gitaly_errors do + gitaly_commit_client.list_last_commits_for_tree(sha, path, offset: offset, limit: limit) + end + end + def last_commit_for_path(sha, path) wrapped_gitaly_errors do gitaly_commit_client.last_commit_for_path(sha, path) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 07e5e204b68..085b2a127a5 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -148,6 +148,24 @@ module Gitlab GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count end + def list_last_commits_for_tree(revision, path, offset: 0, limit: 25) + request = Gitaly::ListLastCommitsForTreeRequest.new( + repository: @gitaly_repo, + revision: encode_binary(revision), + path: encode_binary(path.to_s), + offset: offset, + limit: limit + ) + + response = GitalyClient.call(@repository.storage, :commit_service, :list_last_commits_for_tree, request, timeout: GitalyClient.medium_timeout) + + response.each_with_object({}) do |gitaly_response, hsh| + gitaly_response.commits.each do |commit_for_tree| + hsh[commit_for_tree.path] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit) + end + end + end + def last_commit_for_path(revision, path) request = Gitaly::LastCommitForPathRequest.new( repository: @gitaly_repo, diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 5408a1a6838..0b6cc893db1 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,5 +1,8 @@ module Gitlab class Highlight + TIMEOUT_BACKGROUND = 30.seconds + TIMEOUT_FOREGROUND = 3.seconds + def self.highlight(blob_name, blob_content, repository: nil, plain: false) new(blob_name, blob_content, repository: repository) .highlight(blob_content, continue: false, plain: plain) @@ -51,11 +54,20 @@ module Gitlab end def highlight_rich(text, continue: true) - @formatter.format(lexer.lex(text, continue: continue), tag: lexer.tag).html_safe + tag = lexer.tag + tokens = lexer.lex(text, continue: continue) + Timeout.timeout(timeout_time) { @formatter.format(tokens, tag: tag).html_safe } + rescue Timeout::Error => e + Gitlab::Sentry.track_exception(e) + highlight_plain(text) rescue highlight_plain(text) end + def timeout_time + Sidekiq.server? ? TIMEOUT_BACKGROUND : TIMEOUT_FOREGROUND + end + def link_dependencies(text, highlighted_text) Gitlab::DependencyLinker.link(blob_name, text, highlighted_text) end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index a19b3c88627..2bed470514b 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -147,6 +147,12 @@ excluded_attributes: - :reference - :reference_html - :epic_id + hooks: + - :token + - :encrypted_token + - :encrypted_token_iv + - :encrypted_url + - :encrypted_url_iv methods: labels: diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb index b1bf3ca4143..a03ce07b6a1 100644 --- a/lib/gitlab/repository_cache.rb +++ b/lib/gitlab/repository_cache.rb @@ -29,5 +29,21 @@ module Gitlab def read(key) backend.read(cache_key(key)) end + + def write(key, value) + backend.write(cache_key(key), value) + end + + def fetch_without_caching_false(key, &block) + value = read(key) + return value if value + + value = yield + + # Don't cache false values + write(key, value) if value + + value + end end end diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index 2ec871f0754..d95024fccf7 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -1,23 +1,80 @@ module Gitlab module RepositoryCacheAdapter extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize class_methods do - # Wraps around the given method and caches its output in Redis and an instance - # variable. + # Caches and strongly memoizes the method. # # This only works for methods that do not take any arguments. - def cache_method(name, fallback: nil, memoize_only: false) - original = :"_uncached_#{name}" + # + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + def cache_method(name, fallback: nil) + uncached_name = alias_uncached_method(name) + + define_method(name) do + cache_method_output(name, fallback: fallback) do + __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend + end + end + end - alias_method(original, name) + # Caches truthy values from the method. All values are strongly memoized, + # and cached in RequestStore. + # + # Currently only used to cache `exists?` since stale false values are + # particularly troublesome. This can occur, for example, when an NFS mount + # is temporarily down. + # + # This only works for methods that do not take any arguments. + # + # name - The name of the method to be cached. + def cache_method_asymmetrically(name) + uncached_name = alias_uncached_method(name) define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do - __send__(original) # rubocop:disable GitlabSecurity/PublicSend + cache_method_output_asymmetrically(name) do + __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend end end end + + # Strongly memoizes the method. + # + # This only works for methods that do not take any arguments. + # + # name - The name of the method to be memoized. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. The fallback value + # is not memoized. + def memoize_method(name, fallback: nil) + uncached_name = alias_uncached_method(name) + + define_method(name) do + memoize_method_output(name, fallback: fallback) do + __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + + # Prepends "_uncached_" to the target method name + # + # Returns the uncached method name + def alias_uncached_method(name) + uncached_name = :"_uncached_#{name}" + + alias_method(uncached_name, name) + + uncached_name + end + end + + # RequestStore-backed RepositoryCache to be used. Should be overridden by + # the including class + def request_store_cache + raise NotImplementedError end # RepositoryCache to be used. Should be overridden by the including class @@ -30,65 +87,93 @@ module Gitlab raise NotImplementedError end - # Caches the supplied block both in a cache and in an instance variable. + # Caches and strongly memoizes the supplied block. # - # The cache key and instance variable are named the same way as the value of - # the `key` argument. + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + def cache_method_output(name, fallback: nil, &block) + memoize_method_output(name, fallback: fallback) do + cache.fetch(name, &block) + end + end + + # Caches truthy values from the supplied block. All values are strongly + # memoized, and cached in RequestStore. # - # This method will return `nil` if the corresponding instance variable is also - # set to `nil`. This ensures we don't keep yielding the block when it returns - # `nil`. + # Currently only used to cache `exists?` since stale false values are + # particularly troublesome. This can occur, for example, when an NFS mount + # is temporarily down. # - # key - The name of the key to cache the data in. - # fallback - A value to fall back to in the event of a Git error. - def cache_method_output(key, fallback: nil, memoize_only: false, &block) - ivar = cache_instance_variable_name(key) - - if instance_variable_defined?(ivar) - instance_variable_get(ivar) - else - # If the repository doesn't exist and a fallback was specified we return - # that value inmediately. This saves us Rugged/gRPC invocations. - return fallback unless fallback.nil? || cache.repository.exists? - - begin - value = - if memoize_only - yield - else - cache.fetch(key, &block) - end - - instance_variable_set(ivar, value) - rescue Gitlab::Git::Repository::NoRepository - # Even if the above `#exists?` check passes these errors might still - # occur (for example because of a non-existing HEAD). We want to - # gracefully handle this and not cache anything - fallback + # name - The name of the method to be cached. + def cache_method_output_asymmetrically(name, &block) + memoize_method_output(name) do + request_store_cache.fetch(name) do + cache.fetch_without_caching_false(name, &block) end end end + # Strongly memoizes the supplied block. + # + # name - The name of the method to be memoized. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. The fallback value is + # not memoized. + def memoize_method_output(name, fallback: nil, &block) + no_repository_fallback(name, fallback: fallback) do + strong_memoize(memoizable_name(name), &block) + end + end + + # Returns the fallback value if the repository does not exist + def no_repository_fallback(name, fallback: nil, &block) + # Avoid unnecessary gRPC invocations + return fallback if fallback && fallback_early?(name) + + yield + rescue Gitlab::Git::Repository::NoRepository + # Even if the `#exists?` check in `fallback_early?` passes, these errors + # might still occur (for example because of a non-existing HEAD). We + # want to gracefully handle this and not memoize anything. + fallback + end + # Expires the caches of a specific set of methods def expire_method_caches(methods) - methods.each do |key| - unless cached_methods.include?(key.to_sym) - Rails.logger.error "Requested to expire non-existent method '#{key}' for Repository" + methods.each do |name| + unless cached_methods.include?(name.to_sym) + Rails.logger.error "Requested to expire non-existent method '#{name}' for Repository" next end - cache.expire(key) + cache.expire(name) - ivar = cache_instance_variable_name(key) - - remove_instance_variable(ivar) if instance_variable_defined?(ivar) + clear_memoization(memoizable_name(name)) end + + expire_request_store_method_caches(methods) end private - def cache_instance_variable_name(key) - :"@#{key.to_s.tr('?!', '')}" + def memoizable_name(name) + "#{name.to_s.tr('?!', '')}" + end + + def expire_request_store_method_caches(methods) + methods.each do |name| + request_store_cache.expire(name) + end + end + + # All cached repository methods depend on the existence of a Git repository, + # so if the repository doesn't exist, we already know not to call it. + def fallback_early?(method_name) + # Avoid infinite loop + return false if method_name == :exists? + + !exists? end end end diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb index b05d408b1c0..c2955cd374c 100644 --- a/lib/gitlab/tree_summary.rb +++ b/lib/gitlab/tree_summary.rb @@ -73,25 +73,29 @@ module Gitlab end def fill_last_commits!(entries) - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 - Gitlab::GitalyClient.allow_n_plus_1_calls do - entries.each do |entry| - raw_commit = repository.last_commit_for_path(commit.id, entry_path(entry)) + # Ensure the path is in "path/" format + ensured_path = + if path + File.join(*[path, ""]) + end + + commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit) - if raw_commit - commit = resolve_commit(raw_commit) + entries.each do |entry| + path_key = entry_path(entry) + commit = cache_commit(commits_hsh[path_key]) - entry[:commit] = commit - entry[:commit_path] = commit_path(commit) - end + if commit + entry[:commit] = commit + entry[:commit_path] = commit_path(commit) end end end - def resolve_commit(raw_commit) - return nil unless raw_commit.present? + def cache_commit(commit) + return nil unless commit.present? - resolved_commits[raw_commit.id] ||= ::Commit.new(raw_commit, project) + resolved_commits[commit.id] ||= commit end def commit_path(commit) diff --git a/rubocop/cop/group_public_or_visible_to_user.rb b/rubocop/cop/group_public_or_visible_to_user.rb new file mode 100644 index 00000000000..beda0b7f8ba --- /dev/null +++ b/rubocop/cop/group_public_or_visible_to_user.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# +module RuboCop + module Cop + # Cop that blacklists the usage of Group.public_or_visible_to_user + class GroupPublicOrVisibleToUser < RuboCop::Cop::Cop + MSG = '`Group.public_or_visible_to_user` should be used with extreme care. ' \ + 'Please ensure that you are not using it on its own and that the amount ' \ + 'of rows being filtered is reasonable.' + + def_node_matcher :public_or_visible_to_user?, <<~PATTERN + (send (const nil? :Group) :public_or_visible_to_user ...) + PATTERN + + def on_send(node) + return unless public_or_visible_to_user?(node) + + add_offense(node, location: :expression) + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index ff929c7b6ce..76d6037706e 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -38,3 +38,4 @@ require_relative 'cop/code_reuse/service_class' require_relative 'cop/code_reuse/presenter' require_relative 'cop/code_reuse/serializer' require_relative 'cop/code_reuse/active_record' +require_relative 'cop/group_public_or_visible_to_user' diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb index ea53f89c844..a5f0ec372d8 100755 --- a/scripts/review_apps/automated_cleanup.rb +++ b/scripts/review_apps/automated_cleanup.rb @@ -97,13 +97,13 @@ end automated_cleanup = AutomatedCleanup.new timed('Review apps cleanup') do - automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 5, days_for_delete: 6) + automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 2, days_for_delete: 3) end puts timed('Helm releases cleanup') do - automated_cleanup.perform_helm_releases_cleanup!(days: 7) + automated_cleanup.perform_helm_releases_cleanup!(days: 3) end exit(0) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 7202cee04ea..2b28cfd16cc 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -728,4 +728,80 @@ describe ApplicationController do end end end + + context 'X-GitLab-Custom-Error header' do + before do + sign_in user + end + + context 'given a 422 error page' do + controller do + def index + render 'errors/omniauth_error', layout: 'errors', status: 422 + end + end + + it 'sets a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' + end + end + + context 'given a 500 error page' do + controller do + def index + render 'errors/omniauth_error', layout: 'errors', status: 500 + end + end + + it 'sets a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' + end + end + + context 'given a 200 success page' do + controller do + def index + render 'errors/omniauth_error', layout: 'errors', status: 200 + end + end + + it 'does not set a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to be_nil + end + end + + context 'given a json response' do + controller do + def index + render json: {}, status: :unprocessable_entity + end + end + + it 'does not set a custom header' do + get :index, format: :json + + expect(response.headers['X-GitLab-Custom-Error']).to be_nil + end + end + + context 'given a json response for an html request' do + controller do + def index + render json: {}, status: :unprocessable_entity + end + end + + it 'does not set a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to be_nil + end + end + end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 0d49033c691..5c7415a318d 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -90,6 +90,11 @@ describe Projects::PipelinesController do context 'when performing gitaly calls', :request_store do it 'limits the Gitaly requests' do + # Isolate from test preparation (Repository#exists? is also cached in RequestStore) + RequestStore.end! + RequestStore.clear! + RequestStore.begin! + expect { get_pipelines_index_json } .to change { Gitlab::GitalyClient.get_request_count }.by(2) end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 088ab114df3..76bc93e9766 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -18,6 +18,23 @@ describe 'Issue Detail', :js do end end + context 'when issue description has xss snippet' do + before do + issue.update!(description: '![xss" onload=alert(1);//](a)') + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'should encode the description to prevent xss issues' do + page.within('.issuable-details .detail-page-description') do + expect(page).to have_selector('img', count: 1) + expect(find('img')['onerror']).to be_nil + expect(find('img')['src']).to end_with('/a') + end + end + end + context 'when edited by a user who is later deleted' do before do sign_in(user) diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb index cd5fef8238e..7c71b4c52e0 100644 --- a/spec/features/projects/fork_spec.rb +++ b/spec/features/projects/fork_spec.rb @@ -53,6 +53,18 @@ describe 'Project fork' do expect(current_path).to have_content(/#{user.namespace.name}/i) end + it 'shows avatars when Gravatar is disabled' do + stub_application_setting(gravatar_enabled: false) + + visit project_path(project) + + click_link 'Fork' + + page.within('.fork-thumbnail-container') do + expect(page).to have_css('div.identicon') + end + end + it 'shows the forked project on the list' do visit project_path(project) diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 70d59c6e621..07e674216fa 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -4,6 +4,9 @@ ], "description": "An extension of job.json with more detailed information", "required": [ + "artifact", + "runner", + "runners", "has_trace" ], "properties": { @@ -12,7 +15,7 @@ "trigger": { "$ref": "trigger.json" }, "deployment_status": { "$ref": "deployment_status.json" }, "runner": { "$ref": "runner.json" }, - "runners": { "type": "runners.json" }, + "runners": { "$ref": "runners.json" }, "has_trace": { "type": "boolean" } } } diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 55ee87163f9..aa0442ab847 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -32,18 +32,6 @@ describe AvatarsHelper do end end - context 'when providing a project path' do - it_behaves_like 'resource with a default avatar', 'project' do - let(:resource) { create(:project, name: 'foo') } - let(:helper_args) { [resource.full_path] } - end - - it_behaves_like 'resource with a custom avatar', 'project' do - let(:resource) { create(:project, :public, avatar: File.open(uploaded_image_temp_path)) } - let(:helper_args) { [resource.full_path] } - end - end - context 'when providing a group' do it_behaves_like 'resource with a default avatar', 'group' do let(:resource) { create(:group, name: 'foo') } @@ -55,18 +43,6 @@ describe AvatarsHelper do let(:helper_args) { [resource] } end end - - context 'when providing a group path' do - it_behaves_like 'resource with a default avatar', 'group' do - let(:resource) { create(:group, name: 'foo') } - let(:helper_args) { [resource.full_path] } - end - - it_behaves_like 'resource with a custom avatar', 'group' do - let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } - let(:helper_args) { [resource.full_path] } - end - end end describe '#avatar_icon_for' do diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js index 0e93c5193a1..47d6492a7a6 100644 --- a/spec/javascripts/ide/components/ide_status_bar_spec.js +++ b/spec/javascripts/ide/components/ide_status_bar_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ideStatusBar from '~/ide/components/ide_status_bar.vue'; +import { rightSidebarViews } from '~/ide/constants'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; import { projectData } from '../mock_data'; @@ -64,7 +65,7 @@ describe('ideStatusBar', () => { describe('pipeline status', () => { it('opens right sidebar on clicking icon', done => { - spyOn(vm, 'setRightPane'); + spyOn(vm, 'openRightPane'); Vue.set(vm.$store.state.pipelines, 'latestPipeline', { details: { status: { @@ -80,7 +81,7 @@ describe('ideStatusBar', () => { .then(() => { vm.$el.querySelector('.ide-status-pipeline button').click(); - expect(vm.setRightPane).toHaveBeenCalledWith('pipelines-list'); + expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/ide/components/panes/right_spec.js b/spec/javascripts/ide/components/panes/right_spec.js index c75975d2af6..4899f850cf4 100644 --- a/spec/javascripts/ide/components/panes/right_spec.js +++ b/spec/javascripts/ide/components/panes/right_spec.js @@ -25,7 +25,8 @@ describe('IDE right pane', () => { describe('active', () => { it('renders merge request button as active', done => { - vm.$store.state.rightPane = rightSidebarViews.mergeRequestInfo; + vm.$store.state.rightPane.isOpen = true; + vm.$store.state.rightPane.currentView = rightSidebarViews.mergeRequestInfo.name; vm.$store.state.currentMergeRequestId = '123'; vm.$store.state.currentProjectId = 'gitlab-ce'; vm.$store.state.currentMergeRequestId = 1; @@ -41,20 +42,21 @@ describe('IDE right pane', () => { }, }; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null); - expect( - vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'), - ).toBe('Merge Request'); - - done(); - }); + vm.$nextTick() + .then(() => { + expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null); + expect( + vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'), + ).toBe('Merge Request'); + }) + .then(done) + .catch(done.fail); }); }); describe('click', () => { beforeEach(() => { - spyOn(vm, 'setRightPane'); + spyOn(vm, 'open'); }); it('sets view to merge request', done => { @@ -63,7 +65,7 @@ describe('IDE right pane', () => { vm.$nextTick(() => { vm.$el.querySelector('.ide-sidebar-link').click(); - expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo); + expect(vm.open).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo); done(); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 0e2e246defd..991fb750876 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -319,8 +319,8 @@ describe('RepoEditor', () => { }); }); - it('calls updateDimensions when rightPane is updated', done => { - vm.$store.state.rightPane = 'testing'; + it('calls updateDimensions when rightPane is opened', done => { + vm.$store.state.rightPane.isOpen = true; vm.$nextTick(() => { expect(vm.editor.updateDimensions).toHaveBeenCalled(); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index 3ce9c9fcda1..7e107747346 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -6,6 +6,7 @@ import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state'; import branchesState from '~/ide/stores/modules/branches/state'; import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; +import paneState from '~/ide/stores/modules/pane/state'; export const resetStore = store => { const newState = { @@ -15,6 +16,7 @@ export const resetStore = store => { pipelines: pipelinesState(), branches: branchesState(), fileTemplates: fileTemplatesState(), + rightPane: paneState(), }; store.replaceState(newState); }; diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js index c29dd9f0d06..734233100ab 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js @@ -69,11 +69,17 @@ describe('IDE file templates actions', () => { describe('fetchTemplateTypes', () => { describe('success', () => { + let nextPage; + beforeEach(() => { - mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(200, [ - { - name: 'MIT', - }, + mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(() => [ + 200, + [ + { + name: 'MIT', + }, + ], + { 'X-NEXT-PAGE': nextPage }, ]); }); @@ -116,6 +122,38 @@ describe('IDE file templates actions', () => { done, ); }); + + it('dispatches actions for next page', done => { + nextPage = '2'; + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplateTypes, + null, + state, + [], + [ + { + type: 'requestTemplateTypes', + }, + { + type: 'receiveTemplateTypesSuccess', + payload: [ + { + name: 'MIT', + }, + ], + }, + { + type: 'fetchTemplateTypes', + payload: 2, + }, + ], + done, + ); + }); }); describe('error', () => { diff --git a/spec/javascripts/ide/stores/modules/pane/actions_spec.js b/spec/javascripts/ide/stores/modules/pane/actions_spec.js new file mode 100644 index 00000000000..f150ded6df5 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pane/actions_spec.js @@ -0,0 +1,87 @@ +import * as actions from '~/ide/stores/modules/pane/actions'; +import * as types from '~/ide/stores/modules/pane/mutation_types'; +import testAction from 'spec/helpers/vuex_action_helper'; + +describe('IDE pane module actions', () => { + const TEST_VIEW = { name: 'test' }; + const TEST_VIEW_KEEP_ALIVE = { name: 'test-keep-alive', keepAlive: true }; + + describe('toggleOpen', () => { + it('dispatches open if closed', done => { + testAction( + actions.toggleOpen, + TEST_VIEW, + { isOpen: false }, + [], + [{ type: 'open', payload: TEST_VIEW }], + done, + ); + }); + + it('dispatches close if opened', done => { + testAction( + actions.toggleOpen, + TEST_VIEW, + { isOpen: true }, + [], + [{ type: 'close' }], + done, + ); + }); + }); + + describe('open', () => { + it('commits SET_OPEN', done => { + testAction( + actions.open, + null, + {}, + [{ type: types.SET_OPEN, payload: true }], + [], + done, + ); + }); + + it('commits SET_CURRENT_VIEW if view is given', done => { + testAction( + actions.open, + TEST_VIEW, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name }, + ], + [], + done, + ); + }); + + it('commits KEEP_ALIVE_VIEW if keepAlive is true', done => { + testAction( + actions.open, + TEST_VIEW_KEEP_ALIVE, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + { type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + ], + [], + done, + ); + }); + }); + + describe('close', () => { + it('commits SET_OPEN', done => { + testAction( + actions.close, + null, + {}, + [{ type: types.SET_OPEN, payload: false }], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pane/getters_spec.js b/spec/javascripts/ide/stores/modules/pane/getters_spec.js new file mode 100644 index 00000000000..2060863b5d6 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pane/getters_spec.js @@ -0,0 +1,61 @@ +import * as getters from '~/ide/stores/modules/pane/getters'; +import state from '~/ide/stores/modules/pane/state'; + +describe('IDE pane module getters', () => { + const TEST_VIEW = 'test-view'; + const TEST_KEEP_ALIVE_VIEWS = { + [TEST_VIEW]: true, + }; + + describe('isActiveView', () => { + it('returns true if given view matches currentView', () => { + const result = getters.isActiveView({ currentView: 'A' })('A'); + + expect(result).toBe(true); + }); + + it('returns false if given view does not match currentView', () => { + const result = getters.isActiveView({ currentView: 'A' })('B'); + + expect(result).toBe(false); + }); + }); + + describe('isAliveView', () => { + it('returns true if given view is in keepAliveViews', () => { + const result = getters.isAliveView( + { keepAliveViews: TEST_KEEP_ALIVE_VIEWS }, + {}, + )(TEST_VIEW); + + expect(result).toBe(true); + }); + + it('returns true if given view is active view and open', () => { + const result = getters.isAliveView( + { ...state(), isOpen: true }, + { isActiveView: () => true }, + )(TEST_VIEW); + + expect(result).toBe(true); + }); + + it('returns false if given view is active view and closed', () => { + const result = getters.isAliveView( + state(), + { isActiveView: () => true }, + )(TEST_VIEW); + + expect(result).toBe(false); + }); + + it('returns false if given view is not activeView', () => { + const result = getters.isAliveView( + { ...state(), isOpen: true }, + { isActiveView: () => false }, + )(TEST_VIEW); + + expect(result).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pane/mutations_spec.js b/spec/javascripts/ide/stores/modules/pane/mutations_spec.js new file mode 100644 index 00000000000..b5fcd35912e --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pane/mutations_spec.js @@ -0,0 +1,42 @@ +import state from '~/ide/stores/modules/pane/state'; +import mutations from '~/ide/stores/modules/pane/mutations'; +import * as types from '~/ide/stores/modules/pane/mutation_types'; + +describe('IDE pane module mutations', () => { + const TEST_VIEW = 'test-view'; + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('SET_OPEN', () => { + it('sets isOpen', () => { + mockedState.isOpen = false; + + mutations[types.SET_OPEN](mockedState, true); + + expect(mockedState.isOpen).toBe(true); + }); + }); + + describe('SET_CURRENT_VIEW', () => { + it('sets currentView', () => { + mockedState.currentView = null; + + mutations[types.SET_CURRENT_VIEW](mockedState, TEST_VIEW); + + expect(mockedState.currentView).toEqual(TEST_VIEW); + }); + }); + + describe('KEEP_ALIVE_VIEW', () => { + it('adds entry to keepAliveViews', () => { + mutations[types.KEEP_ALIVE_VIEW](mockedState, TEST_VIEW); + + expect(mockedState.keepAliveViews).toEqual({ + [TEST_VIEW]: true, + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js index 91edb388791..d85354c3681 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -315,29 +315,29 @@ describe('IDE pipelines actions', () => { 'job', mockedState, [{ type: types.SET_DETAIL_JOB, payload: 'job' }], - [{ type: 'setRightPane', payload: 'jobs-detail' }], + [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], done, ); }); - it('dispatches setRightPane as pipeline when job is null', done => { + it('dispatches rightPane/open as pipeline when job is null', done => { testAction( setDetailJob, null, mockedState, [{ type: types.SET_DETAIL_JOB, payload: null }], - [{ type: 'setRightPane', payload: rightSidebarViews.pipelines }], + [{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }], done, ); }); - it('dispatches setRightPane as job', done => { + it('dispatches rightPane/open as job', done => { testAction( setDetailJob, 'job', mockedState, [{ type: types.SET_DETAIL_JOB, payload: 'job' }], - [{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }], + [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], done, ); }); diff --git a/spec/javascripts/issue_show/index_spec.js b/spec/javascripts/issue_show/index_spec.js new file mode 100644 index 00000000000..fa0b426c06c --- /dev/null +++ b/spec/javascripts/issue_show/index_spec.js @@ -0,0 +1,19 @@ +import initIssueableApp from '~/issue_show'; + +describe('Issue show index', () => { + describe('initIssueableApp', () => { + it('should initialize app with no potential XSS attack', () => { + const d = document.createElement('div'); + d.id = 'js-issuable-app-initial-data'; + d.innerHTML = JSON.stringify({ + initialDescriptionHtml: '<img src=x onerror=alert(1)>', + }); + document.body.appendChild(d); + + const alertSpy = spyOn(window, 'alert'); + initIssueableApp(); + + expect(alertSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index c36b607a34e..d193667210b 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -5,13 +5,13 @@ describe('Pagination component', () => { let component; let PaginationComponent; let spy; - let mountComponet; + let mountComponent; beforeEach(() => { spy = jasmine.createSpy('spy'); PaginationComponent = Vue.extend(paginationComp); - mountComponet = function (props) { + mountComponent = function (props) { return new PaginationComponent({ propsData: props, }).$mount(); @@ -20,7 +20,7 @@ describe('Pagination component', () => { describe('render', () => { it('should not render anything', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 1, page: 1, @@ -37,7 +37,7 @@ describe('Pagination component', () => { describe('prev button', () => { it('should be disabled and non clickable', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 2, page: 1, @@ -59,7 +59,7 @@ describe('Pagination component', () => { }); it('should be enabled and clickable', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 3, page: 2, @@ -78,7 +78,7 @@ describe('Pagination component', () => { describe('first button', () => { it('should call the change callback with the first page', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 3, page: 2, @@ -102,7 +102,7 @@ describe('Pagination component', () => { describe('last button', () => { it('should call the change callback with the last page', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 3, page: 2, @@ -126,7 +126,7 @@ describe('Pagination component', () => { describe('next button', () => { it('should be disabled and non clickable', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 5, page: 5, @@ -148,7 +148,7 @@ describe('Pagination component', () => { }); it('should be enabled and clickable', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 4, page: 3, @@ -168,7 +168,7 @@ describe('Pagination component', () => { describe('numbered buttons', () => { it('should render 5 pages', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 4, page: 3, @@ -185,7 +185,7 @@ describe('Pagination component', () => { }); it('should render the spread operator', () => { - component = mountComponet({ + component = mountComponent({ pageInfo: { nextPage: 4, page: 3, diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index ca319679e80..9633caac788 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -11,10 +11,6 @@ describe Backup::Manager do allow(progress).to receive(:puts) allow(progress).to receive(:print) - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - @old_progress = $progress # rubocop:disable Style/GlobalVars $progress = progress # rubocop:disable Style/GlobalVars end diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index c5a854b5660..fdeea814bb2 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -11,10 +11,6 @@ describe Backup::Repository do allow(FileUtils).to receive(:mkdir_p).and_return(true) allow(FileUtils).to receive(:mv).and_return(true) - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - allow_any_instance_of(described_class).to receive(:progress).and_return(progress) end diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index ba8dc68ceda..ed1ebe9ebf6 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -83,6 +83,11 @@ describe Banzai::Filter::RelativeLinkFilter do expect { filter(act) }.not_to raise_error end + it 'does not raise an exception with a space in the path' do + act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)") + expect { filter(act) }.not_to raise_error + end + it 'ignores ref if commit is passed' do doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') ) expect(doc.at_css('a')['href']) diff --git a/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb new file mode 100644 index 00000000000..2a869446753 --- /dev/null +++ b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::EncryptColumns, :migration, schema: 20180910115836 do + let(:model) { Gitlab::BackgroundMigration::Models::EncryptColumns::WebHook } + let(:web_hooks) { table(:web_hooks) } + + let(:plaintext_attrs) do + { + 'encrypted_token' => nil, + 'encrypted_url' => nil, + 'token' => 'secret', + 'url' => 'http://example.com?access_token=secret' + } + end + + let(:encrypted_attrs) do + { + 'encrypted_token' => be_present, + 'encrypted_url' => be_present, + 'token' => nil, + 'url' => nil + } + end + + describe '#perform' do + it 'encrypts columns for the specified range' do + hooks = web_hooks.create([plaintext_attrs] * 5).sort_by(&:id) + + # Encrypt all but the first and last rows + subject.perform(model, [:token, :url], hooks[1].id, hooks[3].id) + + hooks = web_hooks.where(id: hooks.map(&:id)).order(:id) + + aggregate_failures do + expect(hooks[0]).to have_attributes(plaintext_attrs) + expect(hooks[1]).to have_attributes(encrypted_attrs) + expect(hooks[2]).to have_attributes(encrypted_attrs) + expect(hooks[3]).to have_attributes(encrypted_attrs) + expect(hooks[4]).to have_attributes(plaintext_attrs) + end + end + + it 'acquires an exclusive lock for the update' do + relation = double('relation', each: nil) + + expect(model).to receive(:where) { relation } + expect(relation).to receive(:lock) { relation } + + subject.perform(model, [:token, :url], 1, 1) + end + + it 'skips already-encrypted columns' do + values = { + 'encrypted_token' => 'known encrypted token', + 'encrypted_url' => 'known encrypted url', + 'token' => 'token', + 'url' => 'url' + } + + hook = web_hooks.create(values) + + subject.perform(model, [:token, :url], hook.id, hook.id) + + hook.reload + + expect(hook).to have_attributes(values) + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb new file mode 100644 index 00000000000..c7b272cd6ca --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration, schema: 20180916011959 do + let(:migration) { described_class.new } + + let!(:internal_pipeline) { create(:ci_pipeline, source: :web) } + let(:pipelines) { [internal_pipeline, unknown_pipeline].map(&:id) } + + let!(:unknown_pipeline) do + build(:ci_pipeline, source: :unknown) + .tap { |pipeline| pipeline.save(validate: false) } + end + + subject { migration.perform(pipelines.min, pipelines.max) } + + shared_examples 'no changes' do + it 'does not change the pipeline source' do + expect { subject }.not_to change { unknown_pipeline.reload.source } + end + end + + context 'when unknown pipeline is external' do + before do + create(:generic_commit_status, pipeline: unknown_pipeline) + end + + it 'populates the pipeline source' do + subject + + expect(unknown_pipeline.reload.source).to eq('external') + end + + it 'can be repeated without effect' do + subject + + expect { subject }.not_to change { unknown_pipeline.reload.source } + end + end + + context 'when unknown pipeline has just a build' do + before do + create(:ci_build, pipeline: unknown_pipeline) + end + + it_behaves_like 'no changes' + end + + context 'when unknown pipeline has no statuses' do + it_behaves_like 'no changes' + end + + context 'when unknown pipeline has a build and a status' do + before do + create(:generic_commit_status, pipeline: unknown_pipeline) + create(:ci_build, pipeline: unknown_pipeline) + end + + it_behaves_like 'no changes' + end +end diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 9ca960502c8..98f1696badb 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -6,10 +6,10 @@ describe Gitlab::DataBuilder::Pipeline do let(:pipeline) do create(:ci_pipeline, - project: project, - status: 'success', - sha: project.commit.sha, - ref: project.default_branch) + project: project, + status: 'success', + sha: project.commit.sha, + ref: project.default_branch) end let!(:build) { create(:ci_build, pipeline: pipeline) } @@ -20,18 +20,35 @@ describe Gitlab::DataBuilder::Pipeline do let(:build_data) { data[:builds].first } let(:project_data) { data[:project] } - it { expect(attributes).to be_a(Hash) } - it { expect(attributes[:ref]).to eq(pipeline.ref) } - it { expect(attributes[:sha]).to eq(pipeline.sha) } - it { expect(attributes[:tag]).to eq(pipeline.tag) } - it { expect(attributes[:id]).to eq(pipeline.id) } - it { expect(attributes[:status]).to eq(pipeline.status) } - it { expect(attributes[:detailed_status]).to eq('passed') } + it 'has correct attributes' do + expect(attributes).to be_a(Hash) + expect(attributes[:ref]).to eq(pipeline.ref) + expect(attributes[:sha]).to eq(pipeline.sha) + expect(attributes[:tag]).to eq(pipeline.tag) + expect(attributes[:id]).to eq(pipeline.id) + expect(attributes[:status]).to eq(pipeline.status) + expect(attributes[:detailed_status]).to eq('passed') + expect(build_data).to be_a(Hash) + expect(build_data[:id]).to eq(build.id) + expect(build_data[:status]).to eq(build.status) + expect(project_data).to eq(project.hook_attrs(backward: false)) + end - it { expect(build_data).to be_a(Hash) } - it { expect(build_data[:id]).to eq(build.id) } - it { expect(build_data[:status]).to eq(build.status) } + context 'pipeline without variables' do + it 'has empty variables hash' do + expect(attributes[:variables]).to be_a(Array) + expect(attributes[:variables]).to be_empty() + end + end - it { expect(project_data).to eq(project.hook_attrs(backward: false)) } + context 'pipeline with variables' do + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:data) { described_class.build(pipeline) } + let(:attributes) { data[:object_attributes] } + let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1') } + + it { expect(attributes[:variables]).to be_a(Array) } + it { expect(attributes[:variables]).to contain_exactly({ key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1' }) } + end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 3c8cf9c56cc..5d0a603d11d 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -8,6 +8,20 @@ describe Gitlab::Diff::Highlight do let(:diff) { commit.raw_diffs.first } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } + shared_examples 'without inline diffs' do + let(:code) { '<h2 onmouseover="alert(2)">Test</h2>' } + + before do + allow(Gitlab::Diff::InlineDiff).to receive(:for_lines).and_return([]) + allow_any_instance_of(Gitlab::Diff::Line).to receive(:text).and_return(code) + end + + it 'returns html escaped diff text' do + expect(subject[1].rich_text).to eq html_escape(code) + expect(subject[1].rich_text).to be_html_safe + end + end + describe '#highlight' do context "with a diff file" do let(:subject) { described_class.new(diff_file, repository: project.repository).highlight } @@ -38,6 +52,16 @@ describe Gitlab::Diff::Highlight do expect(subject[5].rich_text).to eq(code) end + + context 'when no diff_refs' do + before do + allow(diff_file).to receive(:diff_refs).and_return(nil) + end + + context 'when no inline diffs' do + it_behaves_like 'without inline diffs' + end + end end context "with diff lines" do @@ -93,6 +117,10 @@ describe Gitlab::Diff::Highlight do expect { subject }. to raise_exception(RangeError) end end + + context 'when no inline diffs' do + it_behaves_like 'without inline diffs' + end end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 29e61d15726..88f7099ff3c 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -56,5 +56,22 @@ describe Gitlab::Highlight do described_class.highlight('file.name', 'Contents') end + + context 'timeout' do + subject { described_class.new('file.name', 'Contents') } + + it 'utilizes timeout for web' do + expect(Timeout).to receive(:timeout).with(described_class::TIMEOUT_FOREGROUND).and_call_original + + subject.highlight("Content") + end + + it 'utilizes longer timeout for sidekiq' do + allow(Sidekiq).to receive(:server?).and_return(true) + expect(Timeout).to receive(:timeout).with(described_class::TIMEOUT_BACKGROUND).and_call_original + + subject.highlight("Content") + end + end end end diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 5bd4d6c6a48..0295138fc3a 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -65,6 +65,144 @@ describe Gitlab::RepositoryCacheAdapter do end end + describe '#cache_method_output_asymmetrically', :use_clean_rails_memory_store_caching, :request_store do + let(:request_store_cache) { repository.send(:request_store_cache) } + + context 'with a non-existing repository' do + let(:project) { create(:project) } # No repository + let(:object) { double } + + subject do + repository.cache_method_output_asymmetrically(:cats) do + object.cats_call_stub + end + end + + it 'returns the output of the original method' do + expect(object).to receive(:cats_call_stub).and_return('output') + + expect(subject).to eq('output') + end + end + + context 'with a method throwing a non-existing-repository error' do + subject do + repository.cache_method_output_asymmetrically(:cats) do + raise Gitlab::Git::Repository::NoRepository + end + end + + it 'returns nil' do + expect(subject).to eq(nil) + end + + it 'does not cache the data' do + subject + + expect(repository.instance_variable_defined?(:@cats)).to eq(false) + expect(cache.exist?(:cats)).to eq(false) + end + end + + context 'with an existing repository' do + let(:object) { double } + + context 'when it returns truthy' do + before do + expect(object).to receive(:cats).once.and_return('truthy output') + end + + it 'caches the output in RequestStore' do + expect do + repository.cache_method_output_asymmetrically(:cats) { object.cats } + end.to change { request_store_cache.read(:cats) }.from(nil).to('truthy output') + end + + it 'caches the output in RepositoryCache' do + expect do + repository.cache_method_output_asymmetrically(:cats) { object.cats } + end.to change { cache.read(:cats) }.from(nil).to('truthy output') + end + end + + context 'when it returns false' do + before do + expect(object).to receive(:cats).once.and_return(false) + end + + it 'caches the output in RequestStore' do + expect do + repository.cache_method_output_asymmetrically(:cats) { object.cats } + end.to change { request_store_cache.read(:cats) }.from(nil).to(false) + end + + it 'does NOT cache the output in RepositoryCache' do + expect do + repository.cache_method_output_asymmetrically(:cats) { object.cats } + end.not_to change { cache.read(:cats) }.from(nil) + end + end + end + end + + describe '#memoize_method_output' do + let(:fallback) { 10 } + + context 'with a non-existing repository' do + let(:project) { create(:project) } # No repository + + subject do + repository.memoize_method_output(:cats, fallback: fallback) do + repository.cats_call_stub + end + end + + it 'returns the fallback value' do + expect(subject).to eq(fallback) + end + + it 'avoids calling the original method' do + expect(repository).not_to receive(:cats_call_stub) + + subject + end + + it 'does not set the instance variable' do + subject + + expect(repository.instance_variable_defined?(:@cats)).to eq(false) + end + end + + context 'with a method throwing a non-existing-repository error' do + subject do + repository.memoize_method_output(:cats, fallback: fallback) do + raise Gitlab::Git::Repository::NoRepository + end + end + + it 'returns the fallback value' do + expect(subject).to eq(fallback) + end + + it 'does not set the instance variable' do + subject + + expect(repository.instance_variable_defined?(:@cats)).to eq(false) + end + end + + context 'with an existing repository' do + it 'sets the instance variable' do + repository.memoize_method_output(:cats, fallback: fallback) do + 'block output' + end + + expect(repository.instance_variable_get(:@cats)).to eq('block output') + end + end + end + describe '#expire_method_caches' do it 'expires the caches of the given methods' do expect(cache).to receive(:expire).with(:rendered_readme) diff --git a/spec/lib/gitlab/repository_cache_spec.rb b/spec/lib/gitlab/repository_cache_spec.rb index fc259cf1208..741ee12633f 100644 --- a/spec/lib/gitlab/repository_cache_spec.rb +++ b/spec/lib/gitlab/repository_cache_spec.rb @@ -47,4 +47,89 @@ describe Gitlab::RepositoryCache do expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p) end end + + describe '#fetch_without_caching_false', :use_clean_rails_memory_store_caching do + let(:key) { :foo } + let(:backend) { Rails.cache } + + it 'requires a block' do + expect do + cache.fetch_without_caching_false(key) + end.to raise_error(LocalJumpError) + end + + context 'when the key does not exist in the cache' do + context 'when the result of the block is truthy' do + it 'returns the result of the block' do + result = cache.fetch_without_caching_false(key) { true } + + expect(result).to be true + end + + it 'caches the value' do + expect(backend).to receive(:write).with("#{key}:#{namespace}", true) + + cache.fetch_without_caching_false(key) { true } + end + end + + context 'when the result of the block is falsey' do + let(:p) { -> { false } } + + it 'returns the result of the block' do + result = cache.fetch_without_caching_false(key, &p) + + expect(result).to be false + end + + it 'does not cache the value' do + expect(backend).not_to receive(:write).with("#{key}:#{namespace}", true) + + cache.fetch_without_caching_false(key, &p) + end + end + end + + context 'when the cached value is truthy' do + before do + backend.write("#{key}:#{namespace}", true) + end + + it 'returns the cached value' do + result = cache.fetch_without_caching_false(key) { 'block result' } + + expect(result).to be true + end + + it 'does not execute the block' do + expect do |b| + cache.fetch_without_caching_false(key, &b) + end.not_to yield_control + end + + it 'does not write to the cache' do + expect(backend).not_to receive(:write) + + cache.fetch_without_caching_false(key) { 'block result' } + end + end + + context 'when the cached value is falsey' do + before do + backend.write("#{key}:#{namespace}", false) + end + + it 'returns the result of the block' do + result = cache.fetch_without_caching_false(key) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'writes the truthy value to the cache' do + expect(backend).to receive(:write).with("#{key}:#{namespace}", 'block result') + + cache.fetch_without_caching_false(key) { 'block result' } + end + end + end end diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb index 5ed2f4400bc..fbaa8d47a71 100644 --- a/spec/models/blob_viewer/package_json_spec.rb +++ b/spec/models/blob_viewer/package_json_spec.rb @@ -40,13 +40,14 @@ describe BlobViewer::PackageJson do end context 'when package.json has "private": true' do + let(:homepage) { 'http://example.com' } let(:data) do <<-SPEC.strip_heredoc { "name": "module-name", "version": "10.3.1", "private": true, - "homepage": "myawesomepackage.com" + "homepage": #{homepage.to_json} } SPEC end @@ -54,10 +55,22 @@ describe BlobViewer::PackageJson do subject { described_class.new(blob) } describe '#package_url' do - it 'returns homepage if any' do - expect(subject).to receive(:prepare!) + context 'when the homepage has a valid URL' do + it 'returns homepage URL' do + expect(subject).to receive(:prepare!) + + expect(subject.package_url).to eq(homepage) + end + end + + context 'when the homepage has an invalid URL' do + let(:homepage) { 'javascript:alert()' } + + it 'returns nil' do + expect(subject).to receive(:prepare!) - expect(subject.package_url).to eq('myawesomepackage.com') + expect(subject.package_url).to be_nil + end end end diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb index 889c243c8d8..03d09cb31d6 100644 --- a/spec/models/ci/pipeline_variable_spec.rb +++ b/spec/models/ci/pipeline_variable_spec.rb @@ -5,4 +5,13 @@ describe Ci::PipelineVariable do it { is_expected.to include_module(HasVariable) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_id) } + + describe '#hook_attrs' do + let(:variable) { create(:ci_pipeline_variable, key: 'foo', value: 'bar') } + + subject { variable.hook_attrs } + + it { is_expected.to be_a(Hash) } + it { is_expected.to eq({ key: 'foo', value: 'bar' }) } + end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index c1eac4fa489..81748681528 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -148,9 +148,14 @@ describe Event do let(:admin) { create(:admin) } let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) } + let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) } + let(:personal_snippet) { create(:personal_snippet, :public, author: author) } let(:note_on_commit) { create(:note_on_commit, project: project) } let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } + let(:note_on_project_snippet) { create(:note_on_project_snippet, author: author, noteable: project_snippet, project: project) } + let(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: author, noteable: personal_snippet, project: nil) } + let(:milestone_on_project) { create(:milestone, project: project) } let(:event) { described_class.new(project: project, target: target, author_id: author.id) } before do @@ -268,6 +273,125 @@ describe Event do end end end + + context 'milestone event' do + let(:target) { milestone_on_project } + + it do + expect(event.visible_to_user?(nil)).to be_truthy + expect(event.visible_to_user?(non_member)).to be_truthy + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + + context 'on public project with private issue tracker and merge requests' do + let(:project) { create(:project, :public, :issues_private, :merge_requests_private) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_falsy + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + end + + context 'on private project' do + let(:project) { create(:project, :private) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_falsy + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + end + end + + context 'project snippet note event' do + let(:target) { note_on_project_snippet } + + it do + expect(event.visible_to_user?(nil)).to be_truthy + expect(event.visible_to_user?(non_member)).to be_truthy + expect(event.visible_to_user?(author)).to be_truthy + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + + context 'on public project with private snippets' do + let(:project) { create(:project, :public, :snippets_private) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_falsy + + # Normally, we'd expect the author of a comment to be able to view it. + # However, this doesn't seem to be the case for comments on snippets. + expect(event.visible_to_user?(author)).to be_falsy + + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + end + + context 'on private project' do + let(:project) { create(:project, :private) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_falsy + + # Normally, we'd expect the author of a comment to be able to view it. + # However, this doesn't seem to be the case for comments on snippets. + expect(event.visible_to_user?(author)).to be_falsy + + expect(event.visible_to_user?(member)).to be_truthy + expect(event.visible_to_user?(guest)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + end + end + + context 'personal snippet note event' do + let(:target) { note_on_personal_snippet } + + it do + expect(event.visible_to_user?(nil)).to be_truthy + expect(event.visible_to_user?(non_member)).to be_truthy + expect(event.visible_to_user?(author)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + + context 'on internal snippet' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: author) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_truthy + expect(event.visible_to_user?(author)).to be_truthy + expect(event.visible_to_user?(admin)).to be_truthy + end + end + + context 'on private snippet' do + let(:personal_snippet) { create(:personal_snippet, :private, author: author) } + + it do + expect(event.visible_to_user?(nil)).to be_falsy + expect(event.visible_to_user?(non_member)).to be_falsy + expect(event.visible_to_user?(author)).to be_truthy + + # It is very unexpected that a private personal snippet is not visible + # to an instance administrator. This should be fixed in the future. + expect(event.visible_to_user?(admin)).to be_falsy + end + end + end end describe '.limit_recent' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0729eb99e78..1bf8f89e126 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -169,22 +169,42 @@ describe Group do end end - describe '.visible_to_user' do - let!(:group) { create(:group) } - let!(:user) { create(:user) } + describe '.public_or_visible_to_user' do + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } - subject { described_class.visible_to_user(user) } + subject { described_class.public_or_visible_to_user(user) } - describe 'when the user has access to a group' do - before do - group.add_user(user, Gitlab::Access::MAINTAINER) - end + context 'when user is nil' do + let!(:user) { nil } - it { is_expected.to eq([group]) } + it { is_expected.to match_array([group]) } end - describe 'when the user does not have access to any groups' do - it { is_expected.to eq([]) } + context 'when user' do + let!(:user) { create(:user) } + + context 'when user does not have access to any private group' do + it { is_expected.to match_array([internal_group, group]) } + end + + context 'when user is a member of private group' do + before do + private_group.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to match_array([private_group, internal_group, group]) } + end + + context 'when user is a member of private subgroup', :postgresql do + let!(:private_subgroup) { create(:group, :private, parent: private_group) } + + before do + private_subgroup.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to match_array([private_subgroup, internal_group, group]) } + end end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index a4181631f01..a308ac6e33a 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -57,6 +57,12 @@ describe WebHook do end end + describe 'encrypted attributes' do + subject { described_class.encrypted_attributes.keys } + + it { is_expected.to contain_exactly(:token, :url) } + end + describe 'execute' do let(:data) { { key: 'value' } } let(:hook_name) { 'project hook' } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index dffac05152b..784d17e271e 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -188,6 +188,57 @@ describe Repository do end end + describe '#list_last_commits_for_tree' do + let(:path_to_commit) do + { + "encoding" => "913c66a37b4a45b9769037c55c2d238bd0942d2e", + "files" => "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + ".gitignore" => "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", + ".gitmodules" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "CHANGELOG" => "913c66a37b4a45b9769037c55c2d238bd0942d2e", + "CONTRIBUTING.md" => "6d394385cf567f80a8fd85055db1ab4c5295806f", + "Gemfile.zip" => "ae73cb07c9eeaf35924a10f713b364d32b2dd34f", + "LICENSE" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863", + "MAINTENANCE.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e", + "PROCESS.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e", + "README.md" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863", + "VERSION" => "913c66a37b4a45b9769037c55c2d238bd0942d2e", + "gitlab-shell" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", + "six" => "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" + } + end + + subject { repository.list_last_commits_for_tree(sample_commit.id, '.').id } + + it 'returns the last commits for every entry in the current path' do + result = repository.list_last_commits_for_tree(sample_commit.id, '.') + + result.each do |key, value| + result[key] = value.id + end + + expect(result).to include(path_to_commit) + end + + it 'returns the last commits for every entry in the current path starting from the offset' do + result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size - 1) + + expect(result.size).to eq(1) + end + + it 'returns a limited number of last commits for every entry in the current path starting from the offset' do + result = repository.list_last_commits_for_tree(sample_commit.id, '.', limit: 1) + + expect(result.size).to eq(1) + end + + it 'returns an empty hash when offset is out of bounds' do + result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size) + + expect(result.size).to eq(0) + end + end + describe '#last_commit_for_path' do shared_examples 'getting last commit for path' do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } @@ -1044,6 +1095,47 @@ describe Repository do expect_to_raise_storage_error { broken_repository.exists? } end end + + context 'asymmetric caching', :use_clean_rails_memory_store_caching, :request_store do + let(:cache) { repository.send(:cache) } + let(:request_store_cache) { repository.send(:request_store_cache) } + + context 'when it returns true' do + before do + expect(repository.raw_repository).to receive(:exists?).once.and_return(true) + end + + it 'caches the output in RequestStore' do + expect do + repository.exists? + end.to change { request_store_cache.read(:exists?) }.from(nil).to(true) + end + + it 'caches the output in RepositoryCache' do + expect do + repository.exists? + end.to change { cache.read(:exists?) }.from(nil).to(true) + end + end + + context 'when it returns false' do + before do + expect(repository.raw_repository).to receive(:exists?).once.and_return(false) + end + + it 'caches the output in RequestStore' do + expect do + repository.exists? + end.to change { request_store_cache.read(:exists?) }.from(nil).to(false) + end + + it 'does NOT cache the output in RepositoryCache' do + expect do + repository.exists? + end.not_to change { cache.read(:exists?) }.from(nil) + end + end + end end describe '#has_visible_content?' do @@ -1716,12 +1808,19 @@ describe Repository do describe '#expire_exists_cache' do let(:cache) { repository.send(:cache) } + let(:request_store_cache) { repository.send(:request_store_cache) } it 'expires the cache' do expect(cache).to receive(:expire).with(:exists?) repository.expire_exists_cache end + + it 'expires the request store cache', :request_store do + expect(request_store_cache).to receive(:expire).with(:exists?) + + repository.expire_exists_cache + end end describe '#xcode_project?' do @@ -1892,7 +1991,7 @@ describe Repository do match[1].to_sym if match end.compact - expect(methods).to match_array(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS) + expect(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS).to include(*methods) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 3a8948f8477..3802b5c6848 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -155,7 +155,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq(Group.visible_to_user(user1).order(:name).pluck(:name)) + expect(response_groups).to eq(groups_visible_to_user(user1).order(:name).pluck(:name)) end it "sorts in descending order when passed" do @@ -164,7 +164,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq(Group.visible_to_user(user1).order(name: :desc).pluck(:name)) + expect(response_groups).to eq(groups_visible_to_user(user1).order(name: :desc).pluck(:name)) end it "sorts by path in order_by param" do @@ -173,7 +173,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq(Group.visible_to_user(user1).order(:path).pluck(:name)) + expect(response_groups).to eq(groups_visible_to_user(user1).order(:path).pluck(:name)) end it "sorts by id in the order_by param" do @@ -182,7 +182,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq(Group.visible_to_user(user1).order(:id).pluck(:name)) + expect(response_groups).to eq(groups_visible_to_user(user1).order(:id).pluck(:name)) end it "sorts also by descending id with pagination fix" do @@ -191,7 +191,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(response_groups).to eq(Group.visible_to_user(user1).order(id: :desc).pluck(:name)) + expect(response_groups).to eq(groups_visible_to_user(user1).order(id: :desc).pluck(:name)) end it "sorts identical keys by id for good pagination" do @@ -211,6 +211,10 @@ describe API::Groups do expect(json_response).to be_an Array expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort) end + + def groups_visible_to_user(user) + Group.where(id: user.authorized_groups.select(:id).reorder(nil)) + end end context 'when using owned in the request' do diff --git a/spec/requests/api/redacted_events_spec.rb b/spec/requests/api/redacted_events_spec.rb new file mode 100644 index 00000000000..086dd3df9ba --- /dev/null +++ b/spec/requests/api/redacted_events_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe 'Redacted events in API::Events' do + shared_examples 'private events are redacted' do + it 'redacts events the user does not have access to' do + expect_any_instance_of(Event).to receive(:visible_to_user?).and_call_original + + get api(path), user + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly( + 'project_id' => nil, + 'action_name' => nil, + 'target_id' => nil, + 'target_iid' => nil, + 'target_type' => nil, + 'author_id' => nil, + 'target_title' => 'Confidential event', + 'created_at' => nil, + 'author_username' => nil + ) + end + end + + describe '/users/:id/events' do + let(:project) { create(:project, :public) } + let(:path) { "/users/#{project.owner.id}/events" } + let(:issue) { create(:issue, :confidential, project: project) } + + before do + EventCreateService.new.open_issue(issue, issue.author) + end + + context 'unauthenticated user views another user with private events' do + let(:user) { nil } + + include_examples 'private events are redacted' + end + + context 'authenticated user without access views another user with private events' do + let(:user) { create(:user) } + + include_examples 'private events are redacted' + end + end + + describe '/projects/:id/events' do + let(:project) { create(:project, :public) } + let(:path) { "/projects/#{project.id}/events" } + let(:issue) { create(:issue, :confidential, project: project) } + + before do + EventCreateService.new.open_issue(issue, issue.author) + end + + context 'unauthenticated user views public project' do + let(:user) { nil } + + include_examples 'private events are redacted' + end + + context 'authenticated user without access views public project' do + let(:user) { create(:user) } + + include_examples 'private events are redacted' + end + end +end diff --git a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb new file mode 100644 index 00000000000..7b5235a3da7 --- /dev/null +++ b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/group_public_or_visible_to_user' + +describe RuboCop::Cop::GroupPublicOrVisibleToUser do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of Group.public_or_visible_to_user with a constant receiver' do + inspect_source('Group.public_or_visible_to_user') + + expect(cop.offenses.size).to eq(1) + end + + it 'does not flat the use of public_or_visible_to_user with a constant that is not Group' do + inspect_source('Project.public_or_visible_to_user') + + expect(cop.offenses.size).to eq(0) + end + + it 'does not flag the use of Group.public_or_visible_to_user with a send receiver' do + inspect_source('foo.public_or_visible_to_user') + + expect(cop.offenses.size).to eq(0) + end +end diff --git a/spec/serializers/diff_line_entity_spec.rb b/spec/serializers/diff_line_entity_spec.rb new file mode 100644 index 00000000000..2549f64bcd3 --- /dev/null +++ b/spec/serializers/diff_line_entity_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DiffLineEntity do + include RepoHelpers + + let(:code) { 'hello world' } + let(:line) { Gitlab::Diff::Line.new(code, 'new', 1, nil, 1) } + let(:entity) { described_class.new(line, request: {}) } + + subject { entity.as_json } + + it 'exposes correct attributes' do + expect(subject).to include( + :line_code, :type, :old_line, :new_line, :text, :meta_data, :rich_text + ) + end + + describe '#rich_text' do + let(:code) { '<h2 onmouseover="alert(2)">Test</h2>' } + let(:rich_text_value) { nil } + + before do + line.instance_variable_set(:@rich_text, rich_text_value) + end + + shared_examples 'escapes html tags' do + it do + expect(subject[:rich_text]).to eq html_escape(code) + expect(subject[:rich_text]).to be_html_safe + end + end + + context 'when rich_line is present' do + let(:rich_text_value) { code } + + it_behaves_like 'escapes html tags' + end + + context 'when rich_line is not present' do + it_behaves_like 'escapes html tags' + end + end +end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 986f11410fd..1a565bb734d 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -82,7 +82,7 @@ describe Clusters::Applications::CheckInstallationProgressService do service.execute expect(application).to be_errored - expect(application.status_reason).to eq(errors) + expect(application.status_reason).to eq("Installation failed") end end diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index a744ec30b65..4bd19f5bd79 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -42,7 +42,7 @@ describe Clusters::Applications::InstallService do service.execute expect(application).to be_errored - expect(application.status_reason).to match(/kubernetes error:/i) + expect(application.status_reason).to match('Kubernetes error.') end end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index b987fe45138..051e8c87f39 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -14,6 +14,49 @@ describe Users::BuildService do expect(service.execute).to be_valid end + context 'allowed params' do + let(:params) do + { + access_level: 1, + admin: 1, + avatar: anything, + bio: 1, + can_create_group: 1, + color_scheme_id: 1, + email: 1, + external: 1, + force_random_password: 1, + hide_no_password: 1, + hide_no_ssh_key: 1, + linkedin: 1, + name: 1, + password: 1, + password_automatically_set: 1, + password_expires_at: 1, + projects_limit: 1, + remember_me: 1, + skip_confirmation: 1, + skype: 1, + theme_id: 1, + twitter: 1, + username: 1, + website_url: 1, + private_profile: 1, + organization: 1, + location: 1, + public_email: 1 + } + end + + it 'sets all allowed attributes' do + admin_user # call first so the admin gets created before setting `expect` + + expect(User).to receive(:new).with(hash_including(params)).and_call_original + + service.execute + end + end + context 'with "user_default_external" application setting' do using RSpec::Parameterized::TableSyntax diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index 4b4de540d9e..c8c9c4e773d 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'help/index' do render expect(rendered).to match '8.0.2' - expect(rendered).to have_link('abcdefg', href: 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg') + expect(rendered).to have_link('8.0.2', href: 'https://gitlab.com/gitlab-org/gitlab-ce/tags/v8.0.2') end end diff --git a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb index 8befae39d3a..0206928a211 100644 --- a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb @@ -12,6 +12,7 @@ describe 'projects/merge_requests/creations/_new_submit.html.haml' do assign(:hidden_commit_count, 0) assign(:total_commit_count, merge_request.commits.count) assign(:project, merge_request.target_project) + assign(:mr_presenter, merge_request.present(current_user: merge_request.author)) allow(view).to receive(:can?).and_return(true) allow(view).to receive(:url_for).and_return('#') diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 9b74a7e1946..c13eab30054 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -24,6 +24,7 @@ describe 'projects/merge_requests/edit.html.haml' do before do assign(:project, project) assign(:merge_request, closed_merge_request) + assign(:mr_presenter, closed_merge_request.present(current_user: user)) allow(view).to receive(:can?).and_return(true) allow(view).to receive(:current_user) diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 85b7d16db54..b36aabb9b3d 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -336,7 +336,6 @@ doorkeeper,4.3.2,MIT doorkeeper-openid_connect,1.5.0,MIT dot-prop,4.2.0,MIT dropzone,4.2.0,MIT -dropzonejs-rails,0.7.2,MIT duplexer,0.1.1,MIT duplexer3,0.1.4,New BSD duplexify,3.5.3,MIT |