diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 18:07:55 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 18:07:55 +0000 |
commit | 603c7d4cac5e28bc1c75e50c23ed2cbe56f1aafc (patch) | |
tree | 907f5b8ee1b6f5aad396e95e3327a08400b9e8ea | |
parent | 120f4aaedc8fe830a3f572491d240d8ee6addefb (diff) | |
download | gitlab-ce-603c7d4cac5e28bc1c75e50c23ed2cbe56f1aafc.tar.gz |
Add latest changes from gitlab-org/gitlab@master
89 files changed, 1388 insertions, 577 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 022d79ecf49..14381f63e4b 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -44,7 +44,6 @@ const Api = { mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', adminStatisticsPath: '/api/:version/application/statistics', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', - lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info', environmentsPath: '/api/:version/projects/:id/environments', group(groupId, callback) { @@ -474,14 +473,6 @@ const Api = { return axios.get(url); }, - lsifData(projectPath, commitId, paths) { - const url = Api.buildUrl(this.lsifPath) - .replace(':id', encodeURIComponent(projectPath)) - .replace(':commit_id', commitId); - - return axios.get(url, { params: { paths } }); - }, - environments(id) { const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id)); return axios.get(url); diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue index 0e5f1f0485d..0e0160b9832 100644 --- a/app/assets/javascripts/code_navigation/components/app.vue +++ b/app/assets/javascripts/code_navigation/components/app.vue @@ -7,7 +7,7 @@ export default { Popover, }, computed: { - ...mapState(['currentDefinition', 'currentDefinitionPosition']), + ...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']), }, mounted() { this.blobViewer = document.querySelector('.blob-viewer'); @@ -39,5 +39,6 @@ export default { v-if="currentDefinition" :position="currentDefinitionPosition" :data="currentDefinition" + :definition-path-prefix="definitionPathPrefix" /> </template> diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue index d5bbe430fcd..f216a4c6e6f 100644 --- a/app/assets/javascripts/code_navigation/components/popover.vue +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -14,6 +14,10 @@ export default { type: Object, required: true, }, + definitionPathPrefix: { + type: String, + required: true, + }, }, data() { return { @@ -27,6 +31,11 @@ export default { top: `${this.position.y + this.position.height}px`, }; }, + definitionPath() { + return ( + this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}` + ); + }, }, watch: { position: { @@ -67,8 +76,8 @@ export default { {{ hover.value }} </p> </div> - <div v-if="data.definition_url" class="popover-body"> - <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> + <div v-if="definitionPath" class="popover-body"> + <gl-button :href="definitionPath" target="_blank" class="w-100" variant="default"> {{ __('Go to definition') }} </gl-button> </div> diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js index 5220b1215b8..9b607023f39 100644 --- a/app/assets/javascripts/code_navigation/store/actions.js +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -1,4 +1,4 @@ -import api from '~/api'; +import axios from '~/lib/utils/axios_utils'; import * as types from './mutation_types'; import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; @@ -12,11 +12,10 @@ export default { fetchData({ commit, dispatch, state }) { commit(types.REQUEST_DATA); - api - .lsifData(state.projectPath, state.commitId, [state.blobPath]) + axios + .get(state.codeNavUrl) .then(({ data }) => { - const dataForPath = data[state.blobPath]; - const normalizedData = dataForPath.reduce((acc, d) => { + const normalizedData = data.reduce((acc, d) => { if (d.hover) { acc[`${d.start_line}:${d.start_char}`] = d; addInteractionClass(d); diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js index bb833a5adbc..febb7afe2f8 100644 --- a/app/assets/javascripts/code_navigation/store/mutations.js +++ b/app/assets/javascripts/code_navigation/store/mutations.js @@ -1,10 +1,9 @@ import * as types from './mutation_types'; export default { - [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { - state.projectPath = projectPath; - state.commitId = commitId; - state.blobPath = blobPath; + [types.SET_INITIAL_DATA](state, { codeNavUrl, definitionPathPrefix }) { + state.codeNavUrl = codeNavUrl; + state.definitionPathPrefix = definitionPathPrefix; }, [types.REQUEST_DATA](state) { state.loading = true; diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue index 31f1dec43ad..76821bcd986 100644 --- a/app/assets/javascripts/ide/components/branches/search_list.vue +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapState } from 'vuex'; -import _ from 'underscore'; +import { debounce } from 'lodash'; import { GlLoadingIcon } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import Item from './item.vue'; @@ -39,7 +39,7 @@ export default { loadBranches() { this.fetchBranches({ search: this.search }); }, - searchBranches: _.debounce(function debounceSearch() { + searchBranches: debounce(function debounceSearch() { this.loadBranches(); }, 250), focusSearch() { diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 2581c3e9928..beff95eb47b 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'; import { sprintf, s__ } from '~/locale'; import consts from '../../stores/modules/commit/constants'; @@ -22,7 +22,7 @@ export default { commitToCurrentBranchText() { return sprintf( s__('IDE|Commit to %{branchName} branch'), - { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` }, + { branchName: `<strong class="monospace">${esc(this.currentBranchId)}</strong>` }, false, ); }, diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index 7710bfb49ec..504391ffdc7 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapState } from 'vuex'; -import _ from 'underscore'; +import { throttle } from 'lodash'; import { __ } from '../../../locale'; import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; @@ -53,7 +53,7 @@ export default { this.$refs.buildTrace.scrollTo(0, 0); } }, - scrollBuildLog: _.throttle(function buildLogScrollDebounce() { + scrollBuildLog: throttle(function buildLogScrollDebounce() { const { scrollTop } = this.$refs.buildTrace; const { offsetHeight, scrollHeight } = this.$refs.buildTrace; diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index 5a8face062b..15c08988977 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapState } from 'vuex'; -import _ from 'underscore'; +import { debounce } from 'lodash'; import { GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -59,7 +59,7 @@ export default { loadMergeRequests() { this.fetchMergeRequests({ type: this.type, search: this.search }); }, - searchMergeRequests: _.debounce(function debounceSearch() { + searchMergeRequests: debounce(function debounceSearch() { this.loadMergeRequests(); }, 250), onSearchFocus() { diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue index 7f65d089148..8adf0122fb4 100644 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue @@ -1,6 +1,5 @@ <script> import { mapActions, mapState } from 'vuex'; -import _ from 'underscore'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import ResizablePanel from '../resizable_panel.vue'; @@ -55,7 +54,7 @@ export default { return this.extensionTabs.filter(tab => tab.show); }, tabViews() { - return _.flatten(this.tabs.map(tab => tab.views)); + return this.tabs.map(tab => tab.views).flat(); }, aliveTabViews() { return this.tabViews.filter(view => this.isAliveView(view.name)); diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 3a63fc32639..343b0b6e90c 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { GlLoadingIcon } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; import Icon from '../../../vue_shared/components/icon.vue'; @@ -35,7 +35,7 @@ export default { return sprintf( __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), { - linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`, + linkStart: `<a href="${esc(this.currentProject.web_url)}/-/ci/lint">`, linkEnd: '</a>', }, false, diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index aa8d932da6e..86a773499bc 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { Manager } from 'smooshpack'; import { listen } from 'codesandbox-api'; import { GlLoadingIcon } from '@gitlab/ui'; @@ -78,7 +78,7 @@ export default { .then(() => this.initPreview()); }, beforeDestroy() { - if (!_.isEmpty(this.manager)) { + if (!isEmpty(this.manager)) { this.manager.listener(); } this.manager = {}; @@ -125,7 +125,7 @@ export default { clearTimeout(this.timeout); this.timeout = setTimeout(() => { - if (_.isEmpty(this.manager)) { + if (isEmpty(this.manager)) { this.initPreview(); return; diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 9e9d9df8f82..55a0dd848c8 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { mapActions } from 'vuex'; -import _ from 'underscore'; import Translate from '~/vue_shared/translate'; +import { identity } from 'lodash'; import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; @@ -31,7 +31,7 @@ Vue.use(Translate); export function initIde(el, options = {}) { if (!el) return null; - const { rootComponent = ide, extendStore = _.identity } = options; + const { rootComponent = ide, extendStore = identity } = options; return new Vue({ el, diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index 046e562ba2b..234a7f903a1 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -1,5 +1,5 @@ import { Range } from 'monaco-editor'; -import { throttle } from 'underscore'; +import { throttle } from 'lodash'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 3d729463cb4..3aff4d30d81 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { debounce } from 'lodash'; import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; import store from '../stores'; import DecorationsController from './decorations/controller'; @@ -38,7 +38,7 @@ export default class Editor { setupThemes(); - this.debouncedUpdate = _.debounce(() => { + this.debouncedUpdate = debounce(() => { this.updateDimensions(); }, 200); } diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index ddc0925efb9..04cf0ad53d5 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { __, sprintf } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; @@ -296,7 +296,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force = sprintf( __('Branch not loaded - %{branchId}'), { - branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`, }, false, ), diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 62084892d13..0b168009847 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import flash from '~/flash'; import { __, sprintf } from '~/locale'; import service from '../../services'; @@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { text: sprintf( __("Branch %{branchName} was not found in this project's repository."), { - branchName: `<strong>${_.escape(branchId)}</strong>`, + branchName: `<strong>${esc(branchId)}</strong>`, }, false, ), @@ -154,7 +154,7 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, sprintf( __('An error occurred while getting files for - %{branchId}'), { - branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`, }, false, ), diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 828e4ed5eb9..7d48f0adc4c 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { defer } from 'lodash'; import { __ } from '../../../locale'; import service from '../../services'; import * as types from '../mutation_types'; @@ -71,7 +71,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) => // Defer setting the directory data because this triggers some intense rendering. // The entries is all we need to load the file editor. - _.defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList })); + defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList })); resolve(); }) diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index 92d4be81c75..70b3af8dc75 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -7,17 +7,15 @@ import { GlAlert, GlDropdown, GlDropdownHeader, - GlDropdownDivider, GlDropdownItem, - GlFormGroup, - GlSearchBoxByClick, GlInfiniteScroll, } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; + +import LogSimpleFilters from './log_simple_filters.vue'; +import LogAdvancedFilters from './log_advanced_filters.vue'; import LogControlButtons from './log_control_buttons.vue'; -import { timeRanges, defaultTimeRange } from '~/vue_shared/constants'; +import { defaultTimeRange } from '~/vue_shared/constants'; import { timeRangeFromUrl } from '~/monitoring/utils'; import { formatDate } from '../utils'; @@ -28,12 +26,10 @@ export default { GlAlert, GlDropdown, GlDropdownHeader, - GlDropdownDivider, GlDropdownItem, - GlFormGroup, - GlSearchBoxByClick, GlInfiniteScroll, - DateTimePicker, + LogSimpleFilters, + LogAdvancedFilters, LogControlButtons, }, filters: { @@ -63,49 +59,22 @@ export default { traceHeight: 600, data() { return { - searchQuery: '', - timeRanges, isElasticStackCalloutDismissed: false, scrollDownButtonDisabled: true, }; }, computed: { ...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']), - ...mapGetters('environmentLogs', ['trace']), - - timeRangeModel: { - get() { - return this.timeRange.selected; - }, - set(val) { - this.setTimeRange(val); - }, - }, + ...mapGetters('environmentLogs', ['trace', 'showAdvancedFilters']), showLoader() { return this.logs.isLoading; }, - advancedFeaturesEnabled() { - const environment = this.environments.options.find( - ({ name }) => name === this.environments.current, - ); - return environment && environment.enable_advanced_logs_querying; - }, - disableAdvancedControls() { - return this.environments.isLoading || !this.advancedFeaturesEnabled; - }, shouldShowElasticStackCallout() { - return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls; - }, - - podDropdownText() { - if (this.pods.current) { - return this.pods.current; - } else if (this.advancedFeaturesEnabled) { - // "All pods" is a valid option when advanced querying is available - return s__('Environments|All pods'); - } - return s__('Environments|No pod selected'); + return ( + !this.isElasticStackCalloutDismissed && + (this.environments.isLoading || !this.showAdvancedFilters) + ); }, }, mounted() { @@ -121,7 +90,6 @@ export default { ...mapActions('environmentLogs', [ 'setInitData', 'setSearch', - 'setTimeRange', 'showPodLogs', 'showEnvironment', 'fetchEnvironments', @@ -131,9 +99,6 @@ export default { isCurrentEnvironment(envName) { return envName === this.environments.current; }, - isCurrentPod(podName) { - return podName === this.pods.current; - }, topReached() { if (!this.logs.isLoading) { this.fetchMoreLogsPrepend(); @@ -167,123 +132,49 @@ export default { </strong> </a> </gl-alert> - <div class="top-bar js-top-bar d-flex"> - <div class="row mx-n1"> - <gl-form-group - id="environments-dropdown-fg" - label-size="sm" - label-for="environments-dropdown" - class="col-3 px-1" + <div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2"> + <div class="flex-grow-0"> + <gl-dropdown + id="environments-dropdown" + :text="environments.current" + :disabled="environments.isLoading" + class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown" > - <gl-dropdown - id="environments-dropdown" - :text="environments.current" - :disabled="environments.isLoading" - class="d-flex gl-h-32 js-environments-dropdown" - toggle-class="dropdown-menu-toggle" + <gl-dropdown-header class="text-center"> + {{ s__('Environments|Select environment') }} + </gl-dropdown-header> + <gl-dropdown-item + v-for="env in environments.options" + :key="env.id" + @click="showEnvironment(env.name)" > - <gl-dropdown-header class="text-center"> - {{ s__('Environments|Select environment') }} - </gl-dropdown-header> - <gl-dropdown-item - v-for="env in environments.options" - :key="env.id" - @click="showEnvironment(env.name)" - > - <div class="d-flex"> - <gl-icon - :class="{ invisible: !isCurrentEnvironment(env.name) }" - name="status_success_borderless" - /> - <div class="flex-grow-1">{{ env.name }}</div> - </div> - </gl-dropdown-item> - </gl-dropdown> - </gl-form-group> - - <gl-form-group - id="pods-dropdown-fg" - label-size="sm" - label-for="pods-dropdown" - class="col-3 px-1" - > - <gl-dropdown - id="pods-dropdown" - :text="podDropdownText" - :disabled="environments.isLoading" - class="d-flex gl-h-32 js-pods-dropdown" - toggle-class="dropdown-menu-toggle" - > - <gl-dropdown-header class="text-center"> - {{ s__('Environments|Filter by pod') }} - </gl-dropdown-header> - - <template v-if="advancedFeaturesEnabled"> - <gl-dropdown-item key="all-pods" @click="showPodLogs(null)"> - <div class="d-flex"> - <gl-icon - :class="{ invisible: !isCurrentPod(null) }" - name="status_success_borderless" - /> - <div class="flex-grow-1">{{ s__('Environments|All pods') }}</div> - </div> - </gl-dropdown-item> - <gl-dropdown-divider /> - </template> - - <gl-dropdown-item v-if="!pods.options.length" :disabled="true"> - <span class="text-muted"> - {{ s__('Environments|No pods to display') }} - </span> - </gl-dropdown-item> - <gl-dropdown-item - v-for="podName in pods.options" - :key="podName" - class="text-nowrap" - @click="showPodLogs(podName)" - > - <div class="d-flex"> - <gl-icon - :class="{ invisible: !isCurrentPod(podName) }" - name="status_success_borderless" - /> - <div class="flex-grow-1">{{ podName }}</div> - </div> - </gl-dropdown-item> - </gl-dropdown> - </gl-form-group> - <gl-form-group id="search-fg" label-size="sm" label-for="search" class="col-3 px-1"> - <gl-search-box-by-click - v-model.trim="searchQuery" - :disabled="disableAdvancedControls" - :placeholder="s__('Environments|Search')" - class="js-logs-search" - type="search" - autofocus - @submit="setSearch(searchQuery)" - /> - </gl-form-group> - - <gl-form-group - id="dates-fg" - label-size="sm" - label-for="time-window-dropdown" - class="col-3 px-1" - > - <date-time-picker - ref="dateTimePicker" - v-model="timeRangeModel" - class="w-100 gl-h-32" - right - :disabled="disableAdvancedControls" - :options="timeRanges" - /> - </gl-form-group> + <div class="d-flex"> + <gl-icon + :class="{ invisible: !isCurrentEnvironment(env.name) }" + name="status_success_borderless" + /> + <div class="flex-grow-1">{{ env.name }}</div> + </div> + </gl-dropdown-item> + </gl-dropdown> </div> + <log-advanced-filters + v-if="showAdvancedFilters" + ref="log-advanced-filters" + class="d-md-flex flex-grow-1" + :disabled="environments.isLoading" + /> + <log-simple-filters + v-else + ref="log-simple-filters" + class="d-md-flex flex-grow-1" + :disabled="environments.isLoading" + /> + <log-control-buttons ref="scrollButtons" - class="controllers" + class="flex-grow-0 pr-2 mb-2 controllers" :scroll-down-button-disabled="scrollDownButtonDisabled" @refresh="showPodLogs(pods.current)" @scrollDown="scrollDown" diff --git a/app/assets/javascripts/logs/components/log_advanced_filters.vue b/app/assets/javascripts/logs/components/log_advanced_filters.vue new file mode 100644 index 00000000000..dfbd858bf18 --- /dev/null +++ b/app/assets/javascripts/logs/components/log_advanced_filters.vue @@ -0,0 +1,128 @@ +<script> +import { s__ } from '~/locale'; +import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; +import { mapActions, mapState } from 'vuex'; +import { + GlIcon, + GlDropdown, + GlDropdownHeader, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByClick, +} from '@gitlab/ui'; +import { timeRanges } from '~/vue_shared/constants'; + +export default { + components: { + GlIcon, + GlDropdown, + GlDropdownHeader, + GlDropdownDivider, + GlDropdownItem, + GlSearchBoxByClick, + DateTimePicker, + }, + props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + timeRanges, + searchQuery: '', + }; + }, + computed: { + ...mapState('environmentLogs', ['timeRange', 'pods']), + + timeRangeModel: { + get() { + return this.timeRange.selected; + }, + set(val) { + this.setTimeRange(val); + }, + }, + + podDropdownText() { + return this.pods.current || s__('Environments|All pods'); + }, + }, + methods: { + ...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']), + isCurrentPod(podName) { + return podName === this.pods.current; + }, + }, +}; +</script> +<template> + <div> + <gl-dropdown + ref="podsDropdown" + :text="podDropdownText" + :disabled="disabled" + class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown" + > + <gl-dropdown-header class="text-center"> + {{ s__('Environments|Filter by pod') }} + </gl-dropdown-header> + + <gl-dropdown-item v-if="!pods.options.length" disabled> + <span ref="noPodsMsg" class="text-muted"> + {{ s__('Environments|No pods to display') }} + </span> + </gl-dropdown-item> + + <template v-else> + <gl-dropdown-item ref="allPodsOption" key="all-pods" @click="showPodLogs(null)"> + <div class="d-flex"> + <gl-icon + :class="{ invisible: pods.current !== null }" + name="status_success_borderless" + /> + <div class="flex-grow-1">{{ s__('Environments|All pods') }}</div> + </div> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item + v-for="podName in pods.options" + :key="podName" + class="text-nowrap" + @click="showPodLogs(podName)" + > + <div class="d-flex"> + <gl-icon + :class="{ invisible: !isCurrentPod(podName) }" + name="status_success_borderless" + /> + <div class="flex-grow-1">{{ podName }}</div> + </div> + </gl-dropdown-item> + </template> + </gl-dropdown> + + <gl-search-box-by-click + ref="searchBox" + v-model.trim="searchQuery" + :disabled="disabled" + :placeholder="s__('Environments|Search')" + class="mb-2 pr-2 flex-grow-1" + type="search" + autofocus + @submit="setSearch(searchQuery)" + /> + + <date-time-picker + ref="dateTimePicker" + v-model="timeRangeModel" + :disabled="disabled" + :options="timeRanges" + class="mb-2 gl-h-32 pr-2 d-block date-time-picker-wrapper" + right + /> + </div> +</template> diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue new file mode 100644 index 00000000000..21fe1695624 --- /dev/null +++ b/app/assets/javascripts/logs/components/log_simple_filters.vue @@ -0,0 +1,73 @@ +<script> +import { s__ } from '~/locale'; +import { mapActions, mapState } from 'vuex'; +import { GlIcon, GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlDropdown, + GlDropdownHeader, + GlDropdownItem, + }, + props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + searchQuery: '', + }; + }, + computed: { + ...mapState('environmentLogs', ['pods']), + + podDropdownText() { + return this.pods.current || s__('Environments|No pod selected'); + }, + }, + methods: { + ...mapActions('environmentLogs', ['showPodLogs']), + isCurrentPod(podName) { + return podName === this.pods.current; + }, + }, +}; +</script> +<template> + <div> + <gl-dropdown + ref="podsDropdown" + :text="podDropdownText" + :disabled="disabled" + class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown" + > + <gl-dropdown-header class="text-center"> + {{ s__('Environments|Select pod') }} + </gl-dropdown-header> + + <gl-dropdown-item v-if="!pods.options.length" disabled> + <span ref="noPodsMsg" class="text-muted"> + {{ s__('Environments|No pods to display') }} + </span> + </gl-dropdown-item> + <gl-dropdown-item + v-for="podName in pods.options" + :key="podName" + class="text-nowrap" + @click="showPodLogs(podName)" + > + <div class="d-flex"> + <gl-icon + :class="{ invisible: !isCurrentPod(podName) }" + name="status_success_borderless" + /> + <div class="flex-grow-1">{{ podName }}</div> + </div> + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js index 8770306fdd6..d92969c5389 100644 --- a/app/assets/javascripts/logs/stores/getters.js +++ b/app/assets/javascripts/logs/stores/getters.js @@ -5,5 +5,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) => export const trace = state => state.logs.lines.map(mapTrace).join('\n'); -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; +export const showAdvancedFilters = state => { + const environment = state.environments.options.find( + ({ name }) => name === state.environments.current, + ); + return Boolean(environment?.enable_advanced_logs_querying); +}; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 11687378e20..0ecb38a1ea7 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -379,25 +379,19 @@ } .top-bar { - @include build-trace-top-bar($gl-line-height * 3); - position: relative; - top: 0; - - .dropdown-menu-toggle { - width: 200px; + .date-time-picker-wrapper, + .dropdown-toggle { + @include media-breakpoint-up(md) { + width: 140px; + } - @include media-breakpoint-up(sm) { - width: 300px; + @include media-breakpoint-up(lg) { + width: 160px; } } .controllers { - @include build-controllers(16px, flex-end, true, 2); - } - - .refresh-control { - @include build-controllers(16px, flex-end, true, 0); - margin-left: 2px; + @include build-controllers(16px, flex-end, false, 2); } } diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 00d738a50be..f161d76c623 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -43,6 +43,7 @@ .border-color-blue-300 { border-color: $blue-300; } .border-color-default { border-color: $border-color; } .border-bottom-color-default { border-bottom-color: $border-color; } +.border-radius-default { border-radius: $border-radius-default; } .box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; } .gl-children-ml-sm-3 > * { diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 2f554519632..55817550b4b 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -3,11 +3,10 @@ class Admin::ServicesController < Admin::ApplicationController include ServiceParams - before_action :whitelist_query_limiting, only: [:index] before_action :service, only: [:edit, :update] def index - @services = services_templates + @services = Service.find_or_create_templates end def edit @@ -31,21 +30,8 @@ class Admin::ServicesController < Admin::ApplicationController private # rubocop: disable CodeReuse/ActiveRecord - def services_templates - Service.available_services_names.map do |service_name| - service_template = "#{service_name}_service".camelize.constantize - service_template.where(template: true).first_or_create - end - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord def service @service ||= Service.find_by(id: params[:id], template: true) end # rubocop: enable CodeReuse/ActiveRecord - - def whitelist_query_limiting - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42430') - end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 5788fc17a9b..8c8824ae47f 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController .last_for_path(@repository, @ref, @path).sha end + def set_code_navigation_build + return if Feature.disabled?(:code_navigation, @project) + + artifact = + Ci::JobArtifact + .for_sha(@blob.commit_id, @project.id) + .for_job_name(Ci::Build::CODE_NAVIGATION_JOB_NAME) + .last + + @code_navigation_build = artifact&.job + end + def show_html environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params[:find_latest] = true @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path) + set_code_navigation_build render 'show' end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index da6a0e38c44..2f5aac892ab 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -26,7 +26,7 @@ module MilestonesHelper end end - def milestones_label_path(opts = {}) + def milestones_issues_path(opts = {}) if @project project_issues_path(@project, opts) elsif @group @@ -283,6 +283,27 @@ module MilestonesHelper can?(current_user, :admin_milestone, @project.group) end end + + def display_issues_count_warning?(milestone) + milestone_visible_issues_count(milestone) > Milestone::DISPLAY_ISSUES_LIMIT + end + + def milestone_issues_count_message(milestone) + total_count = milestone_visible_issues_count(milestone) + limit = Milestone::DISPLAY_ISSUES_LIMIT + link_options = { milestone_title: @milestone.title } + + message = _('Showing %{limit} of %{total_count} issues. ') % { limit: limit, total_count: total_count } + message += link_to(_('View all issues'), milestones_issues_path(link_options)) + + message.html_safe + end + + private + + def milestone_visible_issues_count(milestone) + @milestone_visible_issues_count ||= milestone.issues_visible_to_user(current_user).size + end end MilestonesHelper.prepend_if_ee('EE::MilestonesHelper') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b555b78cda6..d0ea7439556 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -33,6 +33,8 @@ module Ci scheduler_failure: 2 }.freeze + CODE_NAVIGATION_JOB_NAME = 'code_navigation' + has_one :deployment, as: :deployable, class_name: 'Deployment' has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_many :trace_sections, class_name: 'Ci::BuildTraceSection' diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index ae57da9c546..ef0701b3874 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -31,7 +31,8 @@ module Ci metrics: 'metrics.txt', lsif: 'lsif.json', dotenv: '.env', - cobertura: 'cobertura-coverage.xml' + cobertura: 'cobertura-coverage.xml', + terraform: 'tfplan.json' }.freeze INTERNAL_TYPES = { @@ -59,7 +60,8 @@ module Ci dast: :raw, license_management: :raw, license_scanning: :raw, - performance: :raw + performance: :raw, + terraform: :raw }.freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze @@ -80,6 +82,7 @@ module Ci scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } + scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) } scope :with_file_types, -> (file_types) do types = self.file_types.select { |file_type| file_types.include?(file_type) }.values @@ -129,7 +132,8 @@ module Ci network_referee: 14, ## runner referees lsif: 15, # LSIF data for code navigation dotenv: 16, - cobertura: 17 + cobertura: 17, + terraform: 18 # Transformed json } enum file_format: { diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 6dbb9649b9f..fac058e5a46 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Milestoneish + DISPLAY_ISSUES_LIMIT = 3000 + def total_issues_count @total_issues_count ||= Milestones::IssuesCountService.new(self).count end @@ -55,7 +57,15 @@ module Milestoneish end def sorted_issues(user) - issues_visible_to_user(user).preload_associated_models.sort_by_attribute('label_priority') + # This method is used on milestone view to filter opened assigned, opened unassigned and closed issues columns. + # We want a limit of DISPLAY_ISSUES_LIMIT for total issues present on all columns. + limited_ids = + issues_visible_to_user(user).sort_by_attribute('label_priority').limit(DISPLAY_ISSUES_LIMIT) + + Issue + .where(id: Issue.select(:id).from(limited_ids)) + .preload_associated_models + .sort_by_attribute('label_priority') end def sorted_merge_requests(user) diff --git a/app/models/issue.rb b/app/models/issue.rb index bdcebb4b942..0f00a78c728 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -70,7 +70,7 @@ class Issue < ApplicationRecord scope :order_closed_date_desc, -> { reorder(closed_at: :desc) } scope :order_created_at_desc, -> { reorder(created_at: :desc) } - scope :preload_associated_models, -> { preload(:labels, project: :namespace) } + scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } scope :public_only, -> { where(confidential: false) } diff --git a/app/models/service.rb b/app/models/service.rb index 5782fab3266..138da0c546e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -46,6 +46,7 @@ class Service < ApplicationRecord scope :active, -> { where(active: true) } scope :without_defaults, -> { where(default: false) } scope :by_type, -> (type) { where(type: type) } + scope :templates, -> { where(template: true, type: available_services_types) } scope :push_hooks, -> { where(push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } @@ -259,14 +260,32 @@ class Service < ApplicationRecord self.category == :issue_tracker end + # Find all service templates; if some of them do not exist, create them + # within a transaction to perform the lowest possible SQL queries. + def self.find_or_create_templates + create_nonexistent_templates + templates + end + + private_class_method def self.create_nonexistent_templates + nonexistent_services = available_services_types - templates.map(&:type) + return if nonexistent_services.empty? + + transaction do + nonexistent_services.each do |service_type| + service_type.constantize.create(template: true) + end + end + end + def self.available_services_names service_names = %w[ alerts asana assembla bamboo - buildkite bugzilla + buildkite campfire custom_issue_tracker discord @@ -278,20 +297,20 @@ class Service < ApplicationRecord hipchat irker jira - mattermost_slash_commands mattermost + mattermost_slash_commands + microsoft_teams packagist pipelines_email pivotaltracker prometheus pushover redmine - youtrack - slack_slash_commands slack + slack_slash_commands teamcity - microsoft_teams unify_circuit + youtrack ] if Rails.env.development? @@ -301,6 +320,10 @@ class Service < ApplicationRecord service_names.sort_by(&:downcase) end + def self.available_services_types + available_services_names.map { |service_name| "#{service_name}_service".camelize } + end + def self.build_from_template(project_id, template) service = template.dup diff --git a/app/uploaders/content_type_whitelist.rb b/app/uploaders/content_type_whitelist.rb index b3975d7e2e0..3210d57b00c 100644 --- a/app/uploaders/content_type_whitelist.rb +++ b/app/uploaders/content_type_whitelist.rb @@ -26,14 +26,14 @@ module ContentTypeWhitelist # Here we override and extend CarrierWave's method that does not parse the # magic headers. def check_content_type_whitelist!(new_file) - new_file.content_type = mime_magic_content_type(new_file.path) + if content_type_whitelist + content_type = mime_magic_content_type(new_file.path) - if content_type_whitelist && !whitelisted_content_type?(new_file.content_type) - message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", ")) - raise CarrierWave::IntegrityError, message + unless whitelisted_content_type?(content_type) + message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", ")) + raise CarrierWave::IntegrityError, message + end end - - super(new_file) end def whitelisted_content_type?(content_type) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 91d1fc06a41..02a327c5a49 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -9,8 +9,9 @@ = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder - - if native_code_navigation_enabled?(@project) - #js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } } + - if @code_navigation_build + - code_nav_url = raw_project_job_artifacts_url(@project, @code_navigation_build, path: "lsif/#{blob.path}") + #js-code-navigation{ data: { code_nav_url: "#{code_nav_url}.json", definition_path_prefix: project_blob_path(@project, @ref) } } %article.file-holder = render 'projects/blob/header', blob: blob = render 'projects/blob/content', blob: blob diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index a8db7f8a556..d7e4f2ed5a0 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -1,6 +1,11 @@ - args = { show_project_name: local_assigns.fetch(:show_project_name, false), show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } +- if display_issues_count_warning?(@milestone) + .flash-container + .flash-warning#milestone-issue-count-warning + = milestone_issues_count_message(@milestone) + .row.prepend-top-default .col-md-4 = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true) diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 4c930b90ce7..6d79b0d31b2 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -3,12 +3,12 @@ - options = { milestone_title: @milestone.title, label_name: label.title } %li.no-border - = render_label(label, tooltip: false, link: milestones_label_path(options)) + = render_label(label, tooltip: false, link: milestones_issues_path(options)) %span.prepend-description-left = markdown_field(label, :description) .float-right.d-none.d-lg-block.d-xl-block - = link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do + = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - = link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do + = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml b/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml new file mode 100644 index 00000000000..2954ca7aece --- /dev/null +++ b/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml @@ -0,0 +1,5 @@ +--- +title: Improve logs filters on mobile, simplify kubernetes API logs filters +merge_request: 27484 +author: +type: added diff --git a/changelogs/unreleased/20820-service-templates-performance.yml b/changelogs/unreleased/20820-service-templates-performance.yml new file mode 100644 index 00000000000..d1a43032d75 --- /dev/null +++ b/changelogs/unreleased/20820-service-templates-performance.yml @@ -0,0 +1,5 @@ +--- +title: Reduce number of SQL queries for service templates +merge_request: 27396 +author: +type: performance diff --git a/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml new file mode 100644 index 00000000000..1f622a00b03 --- /dev/null +++ b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml @@ -0,0 +1,5 @@ +--- +title: Optimize ci builds counters in usage data +merge_request: 27770 +author: +type: performance diff --git a/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml b/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml new file mode 100644 index 00000000000..e6b9528780d --- /dev/null +++ b/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml @@ -0,0 +1,5 @@ +--- +title: Leave upload Content-Type unchaged +merge_request: 27864 +author: +type: fixed diff --git a/changelogs/unreleased/issue_39453.yml b/changelogs/unreleased/issue_39453.yml new file mode 100644 index 00000000000..c82444009ed --- /dev/null +++ b/changelogs/unreleased/issue_39453.yml @@ -0,0 +1,5 @@ +--- +title: Limits issues displayed on milestones +merge_request: 23102 +author: +type: performance diff --git a/changelogs/unreleased/sh-log-redis-calls.yml b/changelogs/unreleased/sh-log-redis-calls.yml new file mode 100644 index 00000000000..acf07c04d4b --- /dev/null +++ b/changelogs/unreleased/sh-log-redis-calls.yml @@ -0,0 +1,5 @@ +--- +title: Log Redis call count and duration to log files +merge_request: 27735 +author: +type: other diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 3c9030e7dbc..ae1a5bbac40 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -28,6 +28,8 @@ def check_changelog_yaml(path) if yaml["merge_request"].nil? && !helper.security_mr? message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}" + elsif yaml["merge_request"] != gitlab.mr_json["iid"] + fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}" end rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias # YAML could not be parsed, fail the build. diff --git a/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb b/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb new file mode 100644 index 00000000000..4f41fc4f478 --- /dev/null +++ b/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexOnUserAndCreatedAtToCiBuilds < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + INDEX_NAME = 'index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build' + + def up + add_concurrent_index :ci_builds, [:user_id, :created_at], where: "type = 'Ci::Build'", name: INDEX_NAME + end + + def down + remove_concurrent_index :ci_builds, INDEX_NAME + end +end diff --git a/db/structure.sql b/db/structure.sql index ba03603a7a8..ee6a249093d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8603,6 +8603,8 @@ CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON public.ci_builds USING b CREATE INDEX index_ci_builds_on_user_id ON public.ci_builds USING btree (user_id); +CREATE INDEX index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build ON public.ci_builds USING btree (user_id, created_at) WHERE ((type)::text = 'Ci::Build'::text); + CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON public.ci_builds USING btree (project_id, status) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))); CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id); @@ -12746,5 +12748,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200318165448'), ('20200318175008'), ('20200319203901'), -('20200323075043'); +('20200323075043'), +('20200323122201'); diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 5276e3a7816..2c9e1bc3cdf 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -757,10 +757,7 @@ Repositories may be moved from one storage location using the [Repository API](../../api/projects.html#edit-project): ```shell -curl --request PUT \ - --header "PRIVATE-TOKEN: <your_access_token>" \ - --data "repository_storage=praefect" \ - https://example.gitlab.com/api/v4/projects/123 +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "repository_storage=praefect" https://example.gitlab.com/api/v4/projects/123 ``` ## Debugging Praefect diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index b45f1ae529d..6fd6c5dd8c1 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -255,7 +255,10 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4] ## Display status of database migrations -To check the status of migrations, you can use the following rake task: +See the [upgrade documentation](../../update/README.md#checking-for-background-migrations-before-upgrading) +for how to check that migrations are complete when upgrading GitLab. + +To check the status of specific migrations, you can use the following rake task: ```shell sudo gitlab-rake db:migrate:status diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 657f29cc789..ec7b4c20462 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -867,43 +867,7 @@ end ## Sidekiq -### Kill a worker's Sidekiq jobs - -```ruby -queue = Sidekiq::Queue.new('repository_import') -queue.each { |job| job.delete if <condition>} -``` - -`<condition>` probably includes references to job arguments, which depend on the type of job in question. - -| queue | worker | job args | -| ----- | ------ | -------- | -| repository_import | RepositoryImportWorker | project_id | -| update_merge_requests | UpdateMergeRequestsWorker | project_id, user_id, oldrev, newrev, ref | - -**Example:** Delete all UpdateMergeRequestsWorker jobs associated with a merge request on project_id 125, -merging branch `ref/heads/my_branch`. - -```ruby -queue = Sidekiq::Queue.new('update_merge_requests') -queue.each { |job| job.delete if job.args[0]==125 and job.args[4]=='ref/heads/my_branch'} -``` - -**Note:** Running jobs will not be killed. Stop Sidekiq before doing this, to get all matching jobs. - -### Enable debug logging of Sidekiq - -```ruby -gitlab_rails['env'] = { - 'SIDEKIQ_LOG_ARGUMENTS' => "1" -} -``` - -Then `gitlab-ctl reconfigure; gitlab-ctl restart sidekiq`. The Sidekiq logs will now include additional data for troubleshooting. - -### Sidekiq kill signals - -See <https://github.com/mperham/sidekiq/wiki/Signals#ttin>. +This content has been moved to the [Troubleshooting Sidekiq docs](./sidekiq.md). ## Redis diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index 172e80bee7d..31e41725834 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -180,6 +180,13 @@ detach exit ``` +## Sidekiq kill signals + +TTIN was described above as the signal to print backtraces for logging, however +Sidekiq responds to other signals as well. For example, TSTP and TERM can be used +to gracefully shut Sidekiq down, see +[the Sidekiq Signals docs](https://github.com/mperham/sidekiq/wiki/Signals#ttin). + ## Check for blocking queries Sometimes the speed at which Sidekiq processes jobs can be so fast that it can @@ -260,9 +267,34 @@ end ### Remove Sidekiq jobs for given parameters (destructive) +The general method to kill jobs conditionally is the following: + +```ruby +queue = Sidekiq::Queue.new('<queue name>') +queue.each { |job| job.delete if <condition>} +``` + +NOTE: **Note:** This will remove jobs that are queued but not started, running jobs will not be killed. Have a look at the section below for cancelling running jobs. + +In the method above, `<queue-name>` is the name of the queue that contains the job(s) you want to delete and `<condition>` will decide which jobs get deleted. + +Commonly, `<condition>` references the job arguments, which depend on the type of job in question. To find the arguments for a specific queue, you can have a look at the `perform` function of the related worker file, commonly found at `/app/workers/<queue-name>_worker.rb`. + +For example, `repository_import` has `project_id` as the job argument, while `update_merge_requests` has `project_id, user_id, oldrev, newrev, ref`. + +NOTE: **Note:** Arguments need to be referenced by their sequence id using `job.args[<id>]` because `job.args` is a list of all arguments provided to the Sidekiq job. + +Here are some examples: + +```ruby +queue = Sidekiq::Queue.new('update_merge_requests') +# In this example, we want to remove any update_merge_requests jobs +# for the Project with ID 125 and ref `ref/heads/my_branch` +queue.each { |job| job.delete if job.args[0] == 125 and job.args[4] == 'ref/heads/my_branch' } +``` + ```ruby -# for jobs like this: -# RepositoryImportWorker.new.perform_async(100) +# Cancelling jobs like: `RepositoryImportWorker.new.perform_async(100)` id_list = [100] queue = Sidekiq::Queue.new('repository_import') diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index f2b59b60fe6..88f8b9b57d2 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -150,8 +150,6 @@ For example: ## Structure -### Organize by topic, not by type - Because we want documentation to be a SSOT, we should [organize by topic, not by type](#organize-by-topic-not-by-type). ### Folder structure overview @@ -619,6 +617,22 @@ do not use this option until further notice. ## Links +Links are important in GitLab documentation. They allow you to [link instead of summarizing](#link-instead-of-summarize) +to help preserve an [SSoT](#why-a-single-source-of-truth) within GitLab documentation. + +We include guidance for links in the following categories: + +- How to set up [anchor links](#anchor-links) for headings. +- How to set up [criteria](#basic-link-criteria) for configuring a link. +- What to set up when [linking to a `help`](../documentation/index.md#linking-to-help) page. +- How to set up [links to internal documentation](#links-to-internal-documentation) for cross-references. +- When to use [links requiring permissions](#links-requiring-permissions). +- How to set up a [link to a video](#link-to-video). +- How to [include links with version text](#text-for-documentation-requiring-version-text). +- How to [link to specific lines of code](#link-to-specific-lines-of-code) + +### Basic link criteria + - Use inline link Markdown markup `[Text](https://example.com)`. It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`. @@ -688,6 +702,19 @@ Example: For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/<issue_number>`. ``` +### Link to specific lines of code + +When linking to specifics lines within a file, link to a commit instead of to the branch. +Lines of code change through time, therefore, linking to a line by using the commit link +ensures the user lands on the line you're referring to. + +- **Do:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/11f17c56d8b7f0b752562d78a4298a3a95b5ce66/.gitlab/issue_templates/Feature%20proposal.md#L3)` +- **Don't:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md#L3).` + +If that linked expression is no longer in that line of the file due to further commits, you +can still search the file for that query. In this case, update the document to ensure it +links to the most recent version of the file. + ## Navigation To indicate the steps of navigation through the UI: @@ -1361,7 +1388,7 @@ on this document. Further explanation is given below. - Every method must have the REST API request. For example: - ``` + ```plaintext GET /projects/:id/repository/branches ``` diff --git a/doc/update/README.md b/doc/update/README.md index 93879efb19e..b183df22589 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -122,13 +122,15 @@ If using GitLab 12.9 and newer, run: sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` -If using GitLab 12.8 and older, run the following using a Rails console: +If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console): ```ruby puts Sidekiq::Queue.new("background_migration").size Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size ``` +--- + **For installations from source** If using GitLab 12.9 and newer, run: @@ -138,13 +140,16 @@ cd /home/git/gitlab sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` -If using GitLab 12.8 and older, run the following using a Rails console: +If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console): ```ruby puts Sidekiq::Queue.new("background_migration").size Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size ``` +There is also a [rake task](../administration/raketasks/maintenance.md#display-status-of-database-migrations) +for displaying the status of each database migration. + ## Upgrading to a new major version Major versions are reserved for backwards incompatible changes. We recommend that diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index d3ca0520674..9e7fae05be7 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -10,10 +10,13 @@ to perform various actions. All statistics are opt-out. You can enable/disable them in the **Admin Area > Settings > Metrics and profiling** section **Usage statistics**. -NOTE: **Note:** +## Network configuration + Allow network traffic from your GitLab instance to IP address `104.196.17.203:443`, to send usage statistics to GitLab Inc. +If your GitLab instance is behind a proxy, set the appropriate [proxy configuration variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html). + ## Version Check **(CORE ONLY)** If enabled, version check will inform you if a new version is available and the diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index fe0c857347d..a00ec6bedeb 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -506,9 +506,11 @@ To use SAST in an offline environment, you need: NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner may try to pull remote images even if a local copy is available. Set GitLab -Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) -in an offline environment if you prefer using only locally available Docker images. +meaning the runner will try to pull Docker images from the GitLab container registry even if a local +copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) +in an offline environment if you prefer using only locally available Docker images. However, we +recommend keeping the pull policy setting to `always` as it will better enable updated scanners to +be utilized within your CI/CD pipelines. ### Make GitLab SAST analyzer images available inside your Docker registry diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index 40d37f3601a..8ccee3b5b2b 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -14,7 +14,7 @@ module Gitlab ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics lsif - dotenv cobertura].freeze + dotenv cobertura terraform].freeze attributes ALLOWED_KEYS @@ -36,6 +36,7 @@ module Gitlab validates :lsif, array_of_strings_or_string: true validates :dotenv, array_of_strings_or_string: true validates :cobertura, array_of_strings_or_string: true + validates :terraform, array_of_strings_or_string: true end end diff --git a/lib/gitlab/grape_logging/loggers/perf_logger.rb b/lib/gitlab/grape_logging/loggers/perf_logger.rb index 7e86b35a215..ca4a702cb94 100644 --- a/lib/gitlab/grape_logging/loggers/perf_logger.rb +++ b/lib/gitlab/grape_logging/loggers/perf_logger.rb @@ -5,30 +5,11 @@ module Gitlab module GrapeLogging module Loggers class PerfLogger < ::GrapeLogging::Loggers::Base - def parameters(_, _) - gitaly_data.merge(rugged_data) - end - - def gitaly_data - gitaly_calls = Gitlab::GitalyClient.get_request_count + include ::Gitlab::InstrumentationHelper - return {} if gitaly_calls.zero? - - { - gitaly_calls: Gitlab::GitalyClient.get_request_count, - gitaly_duration: Gitlab::GitalyClient.query_time_ms - } - end - - def rugged_data - rugged_calls = Gitlab::RuggedInstrumentation.query_count - - return {} if rugged_calls.zero? - - { - rugged_calls: rugged_calls, - rugged_duration_ms: Gitlab::RuggedInstrumentation.query_time_ms - } + def parameters(_, _) + payload = {} + payload.tap { add_instrumentation_data(payload) } end end end diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb new file mode 100644 index 00000000000..f9a6fdc05aa --- /dev/null +++ b/lib/gitlab/instrumentation/redis.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'redis' + +module Gitlab + module Instrumentation + module RedisInterceptor + def call(*args, &block) + start = Time.now + super(*args, &block) + ensure + duration = (Time.now - start) + + if ::RequestStore.active? + ::Gitlab::Instrumentation::Redis.increment_request_count + ::Gitlab::Instrumentation::Redis.add_duration(duration) + ::Gitlab::Instrumentation::Redis.add_call_details(duration, args) + end + end + end + + class Redis + REDIS_REQUEST_COUNT = :redis_request_count + REDIS_CALL_DURATION = :redis_call_duration + REDIS_CALL_DETAILS = :redis_call_details + + def self.get_request_count + ::RequestStore[REDIS_REQUEST_COUNT] || 0 + end + + def self.increment_request_count + ::RequestStore[REDIS_REQUEST_COUNT] ||= 0 + ::RequestStore[REDIS_REQUEST_COUNT] += 1 + end + + def self.detail_store + ::RequestStore[REDIS_CALL_DETAILS] ||= [] + end + + def self.query_time_ms + (self.query_time * 1000).round(2) + end + + def self.query_time + ::RequestStore[REDIS_CALL_DURATION] || 0 + end + + def self.add_duration(duration) + total_time = query_time + duration + ::RequestStore[REDIS_CALL_DURATION] = total_time + end + + def self.add_call_details(duration, args) + return unless Gitlab::PerformanceBar.enabled_for_request? + # redis-rb passes an array (e.g. [:get, key]) + return unless args.length == 1 + + detail_store << { + cmd: args.first, + duration: duration, + backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller) + } + end + end + end +end + +class ::Redis::Client + prepend ::Gitlab::Instrumentation::RedisInterceptor +end diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb index edaa9c645b4..5d4e6a7bdef 100644 --- a/lib/gitlab/instrumentation_helper.rb +++ b/lib/gitlab/instrumentation_helper.rb @@ -4,7 +4,7 @@ module Gitlab module InstrumentationHelper extend self - KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms).freeze + KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms redis_calls redis_duration_ms).freeze def add_instrumentation_data(payload) gitaly_calls = Gitlab::GitalyClient.get_request_count @@ -20,6 +20,13 @@ module Gitlab payload[:rugged_calls] = rugged_calls payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms end + + redis_calls = Gitlab::Instrumentation::Redis.get_request_count + + if redis_calls > 0 + payload[:redis_calls] = redis_calls + payload[:redis_duration_ms] = Gitlab::Instrumentation::Redis.query_time_ms + end end # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the diff --git a/lib/peek/views/detailed_view.rb b/lib/peek/views/detailed_view.rb index 4f3eddaf11b..389f5301079 100644 --- a/lib/peek/views/detailed_view.rb +++ b/lib/peek/views/detailed_view.rb @@ -17,7 +17,7 @@ module Peek end def detail_store - ::Gitlab::SafeRequestStore["#{key}_call_details"] ||= [] + ::Gitlab::SafeRequestStore["#{key}_call_details".to_sym] ||= [] end private diff --git a/lib/peek/views/redis_detailed.rb b/lib/peek/views/redis_detailed.rb index 14cabd62025..79845044d75 100644 --- a/lib/peek/views/redis_detailed.rb +++ b/lib/peek/views/redis_detailed.rb @@ -1,39 +1,5 @@ # frozen_string_literal: true -require 'redis' - -module Gitlab - module Peek - module RedisInstrumented - def call(*args, &block) - start = Time.now - super(*args, &block) - ensure - duration = (Time.now - start) - add_call_details(duration, args) - end - - private - - def add_call_details(duration, args) - return unless Gitlab::PerformanceBar.enabled_for_request? - # redis-rb passes an array (e.g. [:get, key]) - return unless args.length == 1 - - detail_store << { - cmd: args.first, - duration: duration, - backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller) - } - end - - def detail_store - ::Gitlab::SafeRequestStore['redis_call_details'] ||= [] - end - end - end -end - module Peek module Views class RedisDetailed < DetailedView @@ -63,7 +29,3 @@ module Peek end end end - -class Redis::Client - prepend Gitlab::Peek::RedisInstrumented -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e1cff49bee2..48874a6c489 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -220,9 +220,6 @@ msgstr "" msgid "%{authorsName}'s thread" msgstr "" -msgid "%{buy_now_link_start}Buy now!%{link_end}" -msgstr "" - msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" @@ -446,6 +443,9 @@ msgstr "" msgid "%{state} epics" msgstr "" +msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position." +msgstr "" + msgid "%{strong_start}%{branch_count}%{strong_end} Branch" msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches" msgstr[0] "" @@ -584,10 +584,12 @@ msgstr "" msgid "+ %{numberOfHiddenAssignees} more" msgstr "" -msgid "+%{approvers} more approvers" -msgstr "" +msgid "+%d more" +msgid_plural "+%d more" +msgstr[0] "" +msgstr[1] "" -msgid "+%{extraOptionCount} more" +msgid "+%{approvers} more approvers" msgstr "" msgid "+%{tags} more" @@ -2427,12 +2429,6 @@ msgstr "" msgid "Ascending" msgstr "" -msgid "Ask an admin to upload a new license to ensure uninterrupted service." -msgstr "" - -msgid "Ask an admin to upload a new license to restore service." -msgstr "" - msgid "Ask your group maintainer to set up a group Runner." msgstr "" @@ -7825,6 +7821,9 @@ msgstr "" msgid "Environments|Select environment" msgstr "" +msgid "Environments|Select pod" +msgstr "" + msgid "Environments|Show all" msgstr "" @@ -8983,9 +8982,6 @@ msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" -msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}" -msgstr "" - msgid "Forgot your password?" msgstr "" @@ -13412,6 +13408,9 @@ msgstr "" msgid "No webhooks found, add one in the form above." msgstr "" +msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription." +msgstr "" + msgid "No, directly import the existing email addresses and usernames." msgstr "" @@ -16210,12 +16209,6 @@ msgstr "" msgid "Pushes" msgstr "" -msgid "Pushing code and creation of issues and merge requests has been disabled." -msgstr "" - -msgid "Pushing code and creation of issues and merge requests will be disabled on %{disabled_on}." -msgstr "" - msgid "PushoverService|%{user_name} deleted branch \"%{ref}\"." msgstr "" @@ -18227,6 +18220,9 @@ msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Showing %{limit} of %{total_count} issues. " +msgstr "" + msgid "Showing %{pageSize} of %{total} issues" msgstr "" @@ -20177,6 +20173,9 @@ msgstr "" msgid "There was an error updating the dashboard, branch named: %{branch} already exists." msgstr "" +msgid "There was an error updating the stage order. Please try reloading the page." +msgstr "" + msgid "There was an error when reseting email token." msgstr "" @@ -21654,12 +21653,6 @@ msgstr "" msgid "Upload a certificate for your domain with all intermediates" msgstr "" -msgid "Upload a new license in the admin area to ensure uninterrupted service." -msgstr "" - -msgid "Upload a new license in the admin area to restore service." -msgstr "" - msgid "Upload a private key for your certificate" msgstr "" @@ -22236,6 +22229,9 @@ msgstr "" msgid "View Documentation" msgstr "" +msgid "View all issues" +msgstr "" + msgid "View blame prior to this change" msgstr "" @@ -23053,6 +23049,9 @@ msgstr "" msgid "You could not create a new trigger." msgstr "" +msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan." +msgstr "" + msgid "You do not have any subscriptions yet" msgstr "" @@ -23272,6 +23271,9 @@ msgstr "" msgid "YouTube" msgstr "" +msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features." +msgstr "" + msgid "Your Commit Email will be used for web based operations, such as edits and merges." msgstr "" @@ -23392,15 +23394,9 @@ msgstr "" msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email." msgstr "" -msgid "Your license expired on %{expires_at}." -msgstr "" - msgid "Your license is valid from" msgstr "" -msgid "Your license will expire in %{remaining_days}." -msgstr "" - msgid "Your message here" msgstr "" @@ -23431,10 +23427,13 @@ msgstr "" msgid "Your request for access has been queued for review." msgstr "" -msgid "Your trial license expired on %{expires_at}." +msgid "Your subscription expired!" +msgstr "" + +msgid "Your subscription has been downgraded" msgstr "" -msgid "Your trial license will expire in %{remaining_days}." +msgid "Your subscription will expire in %{remaining_days}" msgstr "" msgid "Zoom meeting added" @@ -39,6 +39,7 @@ module QA autoload :MailHog, 'qa/runtime/mail_hog' autoload :IPAddress, 'qa/runtime/ip_address' autoload :Search, 'qa/runtime/search' + autoload :Project, 'qa/runtime/project' autoload :ApplicationSettings, 'qa/runtime/application_settings' module API diff --git a/qa/qa/runtime/project.rb b/qa/qa/runtime/project.rb new file mode 100644 index 00000000000..89edfee1fbe --- /dev/null +++ b/qa/qa/runtime/project.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Project + extend self + extend Support::Api + + def create_project(project_name, api_client, project_description = 'default') + project = Resource::Project.fabricate_via_api! do |project| + project.add_name_uuid = false + project.name = project_name + project.description = project_description + project.api_client = api_client + project.visibility = 'public' + end + project + end + + def push_file_to_project(target_project, file_name, file_content) + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = target_project + push.file_name = file_name + push.file_content = file_content + end + end + + def set_project_visibility(api_client, project_id, visibility) + request = Runtime::API::Request.new(api_client, "/projects/#{project_id}") + response = put request.url, visibility: visibility + response.code.equal?(QA::Support::Api::HTTP_STATUS_OK) + end + end + end +end diff --git a/qa/qa/runtime/search.rb b/qa/qa/runtime/search.rb index 29a71b2815c..f7f87d96e68 100644 --- a/qa/qa/runtime/search.rb +++ b/qa/qa/runtime/search.rb @@ -42,6 +42,22 @@ module QA end end + def elasticsearch_on?(api_client) + elasticsearch_state_request = Runtime::API::Request.new(api_client, '/application/settings') + response = get elasticsearch_state_request.url + + parse_body(response)[:elasticsearch_search] && parse_body(response)[:elasticsearch_indexing] + end + + def disable_elasticsearch(api_client) + disable_elasticsearch_request = Runtime::API::Request.new(api_client, '/application/settings') + put disable_elasticsearch_request.url, elasticsearch_search: false, elasticsearch_indexing: false + end + + def create_search_request(api_client, scope, search_term) + Runtime::API::Request.new(api_client, '/search', scope: scope, search: search_term) + end + def find_code(file_name, search_term) find_target_in_scope('blobs', search_term) do |record| record[:filename] == file_name && record[:data].include?(search_term) diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb index 35801643181..5dde0d57293 100644 --- a/spec/controllers/admin/services_controller_spec.rb +++ b/spec/controllers/admin/services_controller_spec.rb @@ -10,21 +10,14 @@ describe Admin::ServicesController do end describe 'GET #edit' do - let!(:project) { create(:project) } - - Service.available_services_names.each do |service_name| - context "#{service_name}" do - let!(:service) do - service_template = "#{service_name}_service".camelize.constantize - service_template.where(template: true).first_or_create - end + let!(:service) do + create(:jira_service, :template) + end - it 'successfully displays the template' do - get :edit, params: { id: service.id } + it 'successfully displays the template' do + get :edit, params: { id: service.id } - expect(response).to have_gitlab_http_status(:ok) - end - end + expect(response).to have_gitlab_http_status(:ok) end end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 225538dcc45..9fdaa728fd7 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -8,18 +8,17 @@ describe Projects::BlobController do let(:project) { create(:project, :public, :repository) } describe "GET show" do + def request + get(:show, params: { namespace_id: project.namespace, project_id: project, id: id }) + end + render_views context 'with file path' do before do expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original - get(:show, - params: { - namespace_id: project.namespace, - project_id: project, - id: id - }) + request end context "valid branch, valid file" do @@ -119,6 +118,32 @@ describe Projects::BlobController do end end end + + context 'when there is an artifact with code navigation data' do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let!(:job) { create(:ci_build, pipeline: pipeline, name: Ci::Build::CODE_NAVIGATION_JOB_NAME) } + let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) } + + let(:id) { 'master/README.md' } + + it 'assigns code_navigation_build variable' do + request + + expect(assigns[:code_navigation_build]).to eq(job) + end + + context 'when code_navigation feature is disabled' do + before do + stub_feature_flags(code_navigation: false) + end + + it 'does not assign code_navigation_build variable' do + request + + expect(assigns[:code_navigation_build]).to be_nil + end + end + end end describe 'GET diff' do diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb index cbc21dd02e5..d8bb4902087 100644 --- a/spec/features/milestones/user_views_milestone_spec.rb +++ b/spec/features/milestones/user_views_milestone_spec.rb @@ -25,6 +25,37 @@ describe "User views milestone" do expect { visit_milestone }.not_to exceed_query_limit(control) end + context 'limiting milestone issues' do + before_all do + 2.times do + create(:issue, milestone: milestone, project: project) + create(:issue, milestone: milestone, project: project, assignees: [user]) + create(:issue, milestone: milestone, project: project, state: :closed) + end + end + + context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do + it "limits issues to display and shows warning" do + stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3) + + visit(project_milestone_path(project, milestone)) + + expect(page).to have_selector('.issuable-row', count: 3) + expect(page).to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues') + expect(page).to have_link('View all issues', href: project_issues_path(project, { milestone_title: milestone.title })) + end + end + + context 'when issues on milestone are below DISPLAY_ISSUES_LIMIT' do + it 'does not display warning' do + visit(project_milestone_path(project, milestone)) + + expect(page).not_to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues') + expect(page).to have_selector('.issuable-row', count: 6) + end + end + end + private def visit_milestone diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb index 121a8e1705b..2b2327940a5 100644 --- a/spec/features/projects/environments_pod_logs_spec.rb +++ b/spec/features/projects/environments_pod_logs_spec.rb @@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do wait_for_requests page.within('.js-environments-dropdown') do - toggle = find(".dropdown-menu-toggle:not([disabled])") + toggle = find(".dropdown-toggle:not([disabled])") expect(toggle).to have_content(environment.name) @@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do wait_for_requests - page.within('.js-pods-dropdown') do - find(".dropdown-menu-toggle:not([disabled])").click + page.within('.qa-pods-dropdown') do + find(".dropdown-toggle:not([disabled])").click dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])") expect(dropdown_items.size).to eq(1) diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js index cfdc0dcc6cc..d5693cc4173 100644 --- a/spec/frontend/code_navigation/components/app_spec.js +++ b/spec/frontend/code_navigation/components/app_spec.js @@ -16,6 +16,7 @@ function factory(initialState = {}) { state: { ...createState(), ...initialState, + definitionPathPrefix: 'https://test.com/blob/master', }, actions: { fetchData, diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js index ad05504a224..df3bbc7c1c6 100644 --- a/spec/frontend/code_navigation/components/popover_spec.js +++ b/spec/frontend/code_navigation/components/popover_spec.js @@ -1,6 +1,8 @@ import { shallowMount } from '@vue/test-utils'; import Popover from '~/code_navigation/components/popover.vue'; +const DEFINITION_PATH_PREFIX = 'http:/'; + const MOCK_CODE_DATA = Object.freeze({ hover: [ { @@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({ value: 'console.log', }, ], - definition_url: 'http://test.com', + definition_path: 'test.com', }); const MOCK_DOCS_DATA = Object.freeze({ @@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({ value: 'console.log', }, ], - definition_url: 'http://test.com', + definition_path: 'test.com', }); let wrapper; -function factory(position, data) { - wrapper = shallowMount(Popover, { propsData: { position, data } }); +function factory(position, data, definitionPathPrefix) { + wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } }); } describe('Code navigation popover component', () => { @@ -33,14 +35,14 @@ describe('Code navigation popover component', () => { }); it('renders popover', () => { - factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); + factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX); expect(wrapper.element).toMatchSnapshot(); }); describe('code output', () => { it('renders code output', () => { - factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); + factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false); @@ -49,7 +51,7 @@ describe('Code navigation popover component', () => { describe('documentation output', () => { it('renders code output', () => { - factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA); + factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true); diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js index 9c44480ca67..f58dc283ada 100644 --- a/spec/frontend/code_navigation/store/actions_spec.js +++ b/spec/frontend/code_navigation/store/actions_spec.js @@ -27,12 +27,10 @@ describe('Code navigation actions', () => { describe('fetchData', () => { let mock; - const state = { - projectPath: 'gitlab-org/gitlab', - commitId: '123', - blobPath: 'index', - }; - const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info'; + + const codeNavUrl = + 'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json'; + const state = { codeNavUrl }; beforeEach(() => { window.gon = { api_version: '1' }; @@ -45,20 +43,18 @@ describe('Code navigation actions', () => { describe('success', () => { beforeEach(() => { - mock.onGet(apiUrl).replyOnce(200, { - index: [ - { - start_line: 0, - start_char: 0, - hover: { value: '123' }, - }, - { - start_line: 1, - start_char: 0, - hover: null, - }, - ], - }); + mock.onGet(codeNavUrl).replyOnce(200, [ + { + start_line: 0, + start_char: 0, + hover: { value: '123' }, + }, + { + start_line: 1, + start_char: 0, + hover: null, + }, + ]); }); it('commits REQUEST_DATA_SUCCESS with normalized data', done => { @@ -106,7 +102,7 @@ describe('Code navigation actions', () => { describe('error', () => { beforeEach(() => { - mock.onGet(apiUrl).replyOnce(500); + mock.onGet(codeNavUrl).replyOnce(500); }); it('dispatches requestDataError', done => { diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js index 117a2ed2f14..305386f4d0b 100644 --- a/spec/frontend/code_navigation/store/mutations_spec.js +++ b/spec/frontend/code_navigation/store/mutations_spec.js @@ -11,14 +11,12 @@ describe('Code navigation mutations', () => { describe('SET_INITIAL_DATA', () => { it('sets initial data', () => { mutations.SET_INITIAL_DATA(state, { - projectPath: 'test', - commitId: '123', - blobPath: 'index.js', + codeNavUrl: 'https://test.com/builds/1005', + definitionPathPrefix: 'https://test.com/blob/master', }); - expect(state.projectPath).toBe('test'); - expect(state.commitId).toBe('123'); - expect(state.blobPath).toBe('index.js'); + expect(state.codeNavUrl).toBe('https://test.com/builds/1005'); + expect(state.definitionPathPrefix).toBe('https://test.com/blob/master'); }); }); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js index fe142d70698..826d51b24f1 100644 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -9,6 +9,8 @@ import { branches } from '../../mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); +jest.mock('lodash/debounce', () => jest.fn); + describe('IDE branches search list', () => { let wrapper; const fetchBranchesMock = jest.fn(); diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js index 49642153c69..4da987725a1 100644 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ b/spec/frontend/logs/components/environment_logs_spec.js @@ -1,7 +1,5 @@ -import Vue from 'vue'; -import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui'; +import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import EnvironmentLogs from '~/logs/components/environment_logs.vue'; import { createStore } from '~/logs/stores'; @@ -13,7 +11,6 @@ import { mockLogsResult, mockTrace, mockPodName, - mockSearch, mockEnvironmentsEndpoint, mockDocumentationPath, } from '../mock_data'; @@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () => ); describe('EnvironmentLogs', () => { - let EnvironmentLogsComponent; let store; let dispatch; let wrapper; @@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => { const updateControlBtnsMock = jest.fn(); const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown'); - const findPodsDropdown = () => wrapper.find('.js-pods-dropdown'); - const findPodsDropdownItems = () => - findPodsDropdown() - .findAll(GlDropdownItem) - .filter(itm => !itm.attributes('disabled')); - const findSearchBar = () => wrapper.find('.js-logs-search'); - const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); + + const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' }); + const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' }); const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert'); const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' }); @@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => { }; const initWrapper = () => { - wrapper = shallowMount(EnvironmentLogsComponent, { + wrapper = shallowMount(EnvironmentLogs, { propsData, store, stubs: { @@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => { beforeEach(() => { store = createStore(); state = store.state.environmentLogs; - EnvironmentLogsComponent = Vue.extend(EnvironmentLogs); jest.spyOn(store, 'dispatch').mockResolvedValue(); @@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => { expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isEmpty()).toBe(false); - // top bar expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true); - expect(findPodsDropdown().is(GlDropdown)).toBe(true); + expect(findSimpleFilters().exists()).toBe(true); expect(findLogControlButtons().exists()).toBe(true); - expect(findSearchBar().exists()).toBe(true); - expect(findSearchBar().is(GlSearchBoxByClick)).toBe(true); - expect(findTimeRangePicker().exists()).toBe(true); - expect(findTimeRangePicker().is(DateTimePicker)).toBe(true); - - // log trace expect(findInfiniteScroll().exists()).toBe(true); expect(findLogTrace().exists()).toBe(true); }); @@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => { expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0); }); - it('displays a disabled pods dropdown', () => { - expect(findPodsDropdown().attributes('disabled')).toBe('true'); - expect(findPodsDropdownItems()).toHaveLength(0); - }); - - it('displays a disabled search bar', () => { - expect(findSearchBar().exists()).toBe(true); - expect(findSearchBar().attributes('disabled')).toBe('true'); - }); - - it('displays a disabled time window dropdown', () => { - expect(findTimeRangePicker().attributes('disabled')).toBe('true'); - }); - it('does not update buttons state', () => { expect(updateControlBtnsMock).not.toHaveBeenCalled(); }); @@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => { initWrapper(); }); - it('displays a disabled time window dropdown', () => { - expect(findTimeRangePicker().attributes('disabled')).toBe('true'); - }); - - it('displays a disabled search bar', () => { - expect(findSearchBar().attributes('disabled')).toBe('true'); - }); - it('displays an alert to upgrade to ES', () => { expect(findInfoAlert().exists()).toBe(true); }); + + it('displays simple filters for kubernetes logs API', () => { + expect(findSimpleFilters().exists()).toBe(true); + expect(findAdvancedFilters().exists()).toBe(false); + }); }); describe('state with data', () => { @@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => { updateControlBtnsMock.mockReset(); }); - it('displays an enabled search bar', () => { - expect(findSearchBar().attributes('disabled')).toBeFalsy(); - - // input a query and click `search` - findSearchBar().vm.$emit('input', mockSearch); - findSearchBar().vm.$emit('submit'); - - expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, expect.any(Object)); - expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch); - }); - - it('displays an enabled time window dropdown', () => { - expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); - }); - it('does not display an alert to upgrade to ES', () => { expect(findInfoAlert().exists()).toBe(false); }); @@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => { const item = items.at(i); if (item.text() !== mockEnvName) { - expect(item.find(GlIcon).classes()).toContain('invisible'); + expect(item.find(GlIcon).classes('invisible')).toBe(true); } else { - // selected - expect(item.find(GlIcon).classes()).not.toContain('invisible'); + expect(item.find(GlIcon).classes('invisible')).toBe(false); } }); }); - it('populates pods dropdown', () => { - const items = findPodsDropdownItems(); - - expect(findPodsDropdown().props('text')).toBe(mockPodName); - expect(items.length).toBe(mockPods.length + 1); - expect(items.at(0).text()).toBe('All pods'); - mockPods.forEach((pod, i) => { - const item = items.at(i + 1); - expect(item.text()).toBe(pod); - }); + it('displays advanced filters for elasticsearch logs API', () => { + expect(findSimpleFilters().exists()).toBe(false); + expect(findAdvancedFilters().exists()).toBe(true); }); it('shows infinite scroll with height and no content', () => { @@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => { expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); }); - it('dropdown has one pod selected', () => { - const items = findPodsDropdownItems(); - mockPods.forEach((pod, i) => { - const item = items.at(i); - if (item.text() !== mockPodName) { - expect(item.find(GlIcon).classes()).toContain('invisible'); - } else { - // selected - expect(item.find(GlIcon).classes()).not.toContain('invisible'); - } - }); - }); - it('populates logs trace', () => { const trace = findLogTrace(); expect(trace.text().split('\n').length).toBe(mockTrace.length); @@ -371,17 +302,6 @@ describe('EnvironmentLogs', () => { ); }); - it('pod name, trace is refreshed', () => { - const items = findPodsDropdownItems(); - const index = 2; // any pod - - expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); - - items.at(index + 1).vm.$emit('click'); - - expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]); - }); - it('refresh button, trace is refreshed', () => { expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js new file mode 100644 index 00000000000..a6fbc40c2c6 --- /dev/null +++ b/spec/frontend/logs/components/log_advanced_filters_spec.js @@ -0,0 +1,185 @@ +import { GlIcon, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { defaultTimeRange } from '~/vue_shared/constants'; +import { convertToFixedRange } from '~/lib/utils/datetime_range'; +import { createStore } from '~/logs/stores'; +import { mockPods, mockSearch } from '../mock_data'; + +import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue'; + +const module = 'environmentLogs'; + +describe('LogAdvancedFilters', () => { + let store; + let dispatch; + let wrapper; + let state; + + const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' }); + const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' }); + const findPodsDropdownItems = () => + findPodsDropdown() + .findAll(GlDropdownItem) + .filter(item => !item.is('[disabled]')); + const findPodsDropdownItemsSelected = () => + findPodsDropdownItems() + .filter(item => { + return !item.find(GlIcon).classes('invisible'); + }) + .at(0); + const findSearchBox = () => wrapper.find({ ref: 'searchBox' }); + const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); + + const mockStateLoading = () => { + state.timeRange.selected = defaultTimeRange; + state.timeRange.current = convertToFixedRange(defaultTimeRange); + state.pods.options = []; + state.pods.current = null; + }; + + const mockStateWithData = () => { + state.timeRange.selected = defaultTimeRange; + state.timeRange.current = convertToFixedRange(defaultTimeRange); + state.pods.options = mockPods; + state.pods.current = null; + }; + + const initWrapper = (propsData = {}) => { + wrapper = shallowMount(LogAdvancedFilters, { + propsData: { + ...propsData, + }, + store, + }); + }; + + beforeEach(() => { + store = createStore(); + state = store.state.environmentLogs; + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + + dispatch = store.dispatch; + }); + + afterEach(() => { + store.dispatch.mockReset(); + + if (wrapper) { + wrapper.destroy(); + } + }); + + it('displays UI elements', () => { + initWrapper(); + + expect(wrapper.isVueInstance()).toBe(true); + expect(wrapper.isEmpty()).toBe(false); + + expect(findPodsDropdown().exists()).toBe(true); + expect(findSearchBox().exists()).toBe(true); + expect(findTimeRangePicker().exists()).toBe(true); + }); + + describe('disabled state', () => { + beforeEach(() => { + mockStateLoading(); + initWrapper({ + disabled: true, + }); + }); + + it('displays disabled filters', () => { + expect(findPodsDropdown().props('text')).toBe('All pods'); + expect(findPodsDropdown().attributes('disabled')).toBeTruthy(); + expect(findSearchBox().attributes('disabled')).toBeTruthy(); + expect(findTimeRangePicker().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('when the state is loading', () => { + beforeEach(() => { + mockStateLoading(); + initWrapper(); + }); + + it('displays a enabled filters', () => { + expect(findPodsDropdown().props('text')).toBe('All pods'); + expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); + expect(findSearchBox().attributes('disabled')).toBeFalsy(); + expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); + }); + + it('displays an empty pods dropdown', () => { + expect(findPodsNoPodsText().exists()).toBe(true); + expect(findPodsDropdownItems()).toHaveLength(0); + }); + }); + + describe('when the state has data', () => { + beforeEach(() => { + mockStateWithData(); + initWrapper(); + }); + + it('displays an enabled pods dropdown', () => { + expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); + expect(findPodsDropdown().props('text')).toBe('All pods'); + }); + + it('displays options in a pods dropdown', () => { + const items = findPodsDropdownItems(); + expect(items).toHaveLength(mockPods.length + 1); + }); + + it('displays "all pods" selected in a pods dropdown', () => { + const selected = findPodsDropdownItemsSelected(); + + expect(selected.text()).toBe('All pods'); + }); + + it('displays options in date time picker', () => { + const options = findTimeRangePicker().props('options'); + + expect(options).toEqual(expect.any(Array)); + expect(options.length).toBeGreaterThan(0); + }); + + describe('when the user interacts', () => { + it('clicks on a all options, showPodLogs is dispatched with null', () => { + const items = findPodsDropdownItems(); + items.at(0).vm.$emit('click'); + + expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null); + }); + + it('clicks on a pod name, showPodLogs is dispatched with pod name', () => { + const items = findPodsDropdownItems(); + const index = 2; // any pod + + items.at(index + 1).vm.$emit('click'); // skip "All pods" option + + expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]); + }); + + it('clicks on search, a serches is done', () => { + expect(findSearchBox().attributes('disabled')).toBeFalsy(); + + // input a query and click `search` + findSearchBox().vm.$emit('input', mockSearch); + findSearchBox().vm.$emit('submit'); + + expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch); + }); + + it('selects a new time range', () => { + expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); + + const mockRange = { start: 'START_DATE', end: 'END_DATE' }; + findTimeRangePicker().vm.$emit('input', mockRange); + + expect(dispatch).toHaveBeenCalledWith(`${module}/setTimeRange`, mockRange); + }); + }); + }); +}); diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js new file mode 100644 index 00000000000..13504a2b1fc --- /dev/null +++ b/spec/frontend/logs/components/log_simple_filters_spec.js @@ -0,0 +1,138 @@ +import { GlIcon, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/logs/stores'; +import { mockPods, mockPodName } from '../mock_data'; + +import LogSimpleFilters from '~/logs/components/log_simple_filters.vue'; + +const module = 'environmentLogs'; + +describe('LogSimpleFilters', () => { + let store; + let dispatch; + let wrapper; + let state; + + const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' }); + const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' }); + const findPodsDropdownItems = () => + findPodsDropdown() + .findAll(GlDropdownItem) + .filter(item => !item.is('[disabled]')); + + const mockPodsLoading = () => { + state.pods.options = []; + state.pods.current = null; + }; + + const mockPodsLoaded = () => { + state.pods.options = mockPods; + state.pods.current = mockPodName; + }; + + const initWrapper = (propsData = {}) => { + wrapper = shallowMount(LogSimpleFilters, { + propsData: { + ...propsData, + }, + store, + }); + }; + + beforeEach(() => { + store = createStore(); + state = store.state.environmentLogs; + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + + dispatch = store.dispatch; + }); + + afterEach(() => { + store.dispatch.mockReset(); + + if (wrapper) { + wrapper.destroy(); + } + }); + + it('displays UI elements', () => { + initWrapper(); + + expect(wrapper.isVueInstance()).toBe(true); + expect(wrapper.isEmpty()).toBe(false); + + expect(findPodsDropdown().exists()).toBe(true); + }); + + describe('disabled state', () => { + beforeEach(() => { + mockPodsLoading(); + initWrapper({ + disabled: true, + }); + }); + + it('displays a disabled pods dropdown', () => { + expect(findPodsDropdown().props('text')).toBe('No pod selected'); + expect(findPodsDropdown().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('loading state', () => { + beforeEach(() => { + mockPodsLoading(); + initWrapper(); + }); + + it('displays an enabled pods dropdown', () => { + expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); + expect(findPodsDropdown().props('text')).toBe('No pod selected'); + }); + + it('displays an empty pods dropdown', () => { + expect(findPodsNoPodsText().exists()).toBe(true); + expect(findPodsDropdownItems()).toHaveLength(0); + }); + }); + + describe('pods available state', () => { + beforeEach(() => { + mockPodsLoaded(); + initWrapper(); + }); + + it('displays an enabled pods dropdown', () => { + expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); + expect(findPodsDropdown().props('text')).toBe(mockPods[0]); + }); + + it('displays a pods dropdown with items', () => { + expect(findPodsNoPodsText().exists()).toBe(false); + expect(findPodsDropdownItems()).toHaveLength(mockPods.length); + }); + + it('dropdown has one pod selected', () => { + const items = findPodsDropdownItems(); + mockPods.forEach((pod, i) => { + const item = items.at(i); + if (item.text() !== mockPodName) { + expect(item.find(GlIcon).classes('invisible')).toBe(true); + } else { + expect(item.find(GlIcon).classes('invisible')).toBe(false); + } + }); + }); + + it('when the user clicks on a pod, showPodLogs is dispatched', () => { + const items = findPodsDropdownItems(); + const index = 2; // any pod + + expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); + + items.at(index).vm.$emit('click'); + + expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]); + }); + }); +}); diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js index fdce575fa97..9d213d8c01f 100644 --- a/spec/frontend/logs/stores/getters_spec.js +++ b/spec/frontend/logs/stores/getters_spec.js @@ -1,7 +1,7 @@ -import * as getters from '~/logs/stores/getters'; +import { trace, showAdvancedFilters } from '~/logs/stores/getters'; import logsPageState from '~/logs/stores/state'; -import { mockLogsResult, mockTrace } from '../mock_data'; +import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data'; describe('Logs Store getters', () => { let state; @@ -13,7 +13,7 @@ describe('Logs Store getters', () => { describe('trace', () => { describe('when state is initialized', () => { it('returns an empty string', () => { - expect(getters.trace(state)).toEqual(''); + expect(trace(state)).toEqual(''); }); }); @@ -23,7 +23,7 @@ describe('Logs Store getters', () => { }); it('returns an empty string', () => { - expect(getters.trace(state)).toEqual(''); + expect(trace(state)).toEqual(''); }); }); @@ -33,7 +33,42 @@ describe('Logs Store getters', () => { }); it('returns an empty string', () => { - expect(getters.trace(state)).toEqual(mockTrace.join('\n')); + expect(trace(state)).toEqual(mockTrace.join('\n')); + }); + }); + }); + + describe('showAdvancedFilters', () => { + describe('when no environments are set', () => { + beforeEach(() => { + state.environments.current = mockEnvName; + state.environments.options = []; + }); + + it('returns false', () => { + expect(showAdvancedFilters(state)).toBe(false); + }); + }); + + describe('when the environment supports filters', () => { + beforeEach(() => { + state.environments.current = mockEnvName; + state.environments.options = mockEnvironments; + }); + + it('returns true', () => { + expect(showAdvancedFilters(state)).toBe(true); + }); + }); + + describe('when the environment does not support filters', () => { + beforeEach(() => { + state.environments.options = mockEnvironments; + state.environments.current = mockEnvironments[1].name; + }); + + it('returns true', () => { + expect(showAdvancedFilters(state)).toBe(false); }); }); }); diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 2c8f76c8f34..9bba3eb2b77 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do :lsif | 'lsif.json' :dotenv | 'build.dotenv' :cobertura | 'cobertura-coverage.xml' + :terraform | 'tfplan.json' end with_them do diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb new file mode 100644 index 00000000000..6f20b8877e0 --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::GrapeLogging::Loggers::PerfLogger do + subject { described_class.new } + + describe ".parameters" do + let(:mock_request) { OpenStruct.new(env: {}) } + + describe 'when no performance datais are present' do + it 'returns an empty Hash' do + expect(subject.parameters(mock_request, nil)).to eq({}) + end + end + + describe 'when Redis calls are present', :request_store do + it 'returns a Hash with Redis information' do + Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') } + + payload = subject.parameters(mock_request, nil) + + expect(payload[:redis_calls]).to eq(1) + expect(payload[:redis_duration_ms]).to be >= 0 + end + end + end +end diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index c2674638743..9788c9f4a3c 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -1,11 +1,51 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' require 'rspec-parameterized' describe Gitlab::InstrumentationHelper do using RSpec::Parameterized::TableSyntax + describe '.add_instrumentation_data', :request_store do + let(:payload) { {} } + + subject { described_class.add_instrumentation_data(payload) } + + it 'adds nothing' do + subject + + expect(payload).to eq({}) + end + + context 'when Gitaly calls are made' do + it 'adds Gitaly data and omits Redis data' do + project = create(:project) + RequestStore.clear! + project.repository.exists? + + subject + + expect(payload[:gitaly_calls]).to eq(1) + expect(payload[:gitaly_duration]).to be >= 0 + expect(payload[:redis_calls]).to be_nil + expect(payload[:redis_duration_ms]).to be_nil + end + end + + context 'when Redis calls are made' do + it 'adds Redis data and omits Gitaly data' do + Gitlab::Redis::Cache.with { |redis| redis.get('test-instrumentation') } + + subject + + expect(payload[:redis_calls]).to eq(1) + expect(payload[:redis_duration_ms]).to be >= 0 + expect(payload[:gitaly_calls]).to be_nil + expect(payload[:gitaly_duration]).to be_nil + end + end + end + describe '.queue_duration_for_job' do where(:enqueued_at, :created_at, :time_now, :expected_duration) do "2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index bd04d30f85f..aab63ba88ad 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do end end - context 'with Gitaly and Rugged calls' do + context 'with Gitaly, Rugged, and Redis calls' do let(:timing_data) do { gitaly_calls: 10, gitaly_duration: 10000, rugged_calls: 1, - rugged_duration_ms: 5000 + rugged_duration_ms: 5000, + redis_calls: 3, + redis_duration_ms: 1234 } end - before do - job.merge!(timing_data) + let(:expected_end_payload) do + end_payload.except('args').merge(timing_data) end it 'logs with Gitaly and Rugged timing data' do Timecop.freeze(timestamp) do expect(logger).to receive(:info).with(start_payload.except('args')).ordered - expect(logger).to receive(:info).with(end_payload.except('args')).ordered + expect(logger).to receive(:info).with(expected_end_payload).ordered - subject.call(job, 'test_queue') { } + subject.call(job, 'test_queue') do + job.merge!(timing_data) + end end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index de93c3c1675..6f6ff3704b4 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -140,6 +140,18 @@ describe Ci::JobArtifact do end end + describe '.for_job_name' do + it 'returns job artifacts for a given job name' do + first_job = create(:ci_build, name: 'first') + second_job = create(:ci_build, name: 'second') + first_artifact = create(:ci_job_artifact, job: first_job) + second_artifact = create(:ci_job_artifact, job: second_job) + + expect(described_class.for_job_name(first_job.name)).to eq([first_artifact]) + expect(described_class.for_job_name(second_job.name)).to eq([second_artifact]) + end + end + describe 'callbacks' do subject { create(:ci_job_artifact, :archive) } diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index cff607a4731..5808d6e37e5 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do end describe '#sorted_issues' do - it 'sorts issues by label priority' do + before do issue.labels << label_1 security_issue_1.labels << label_2 closed_issue_1.labels << label_3 + end + it 'sorts issues by label priority' do issues = milestone.sorted_issues(member) expect(issues.first).to eq(issue) expect(issues.second).to eq(security_issue_1) expect(issues.third).not_to eq(closed_issue_1) end + + it 'limits issue count and keeps the ordering' do + stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 4) + + issues = milestone.sorted_issues(member) + # Cannot use issues.count here because it is sorting + # by a virtual column 'highest_priority' and it will break + # the query. + total_issues_count = issues.opened.unassigned.length + issues.opened.assigned.length + issues.closed.length + expect(issues.length).to eq(4) + expect(total_issues_count).to eq(4) + expect(issues.first).to eq(issue) + expect(issues.second).to eq(security_issue_1) + expect(issues.third).not_to eq(closed_issue_1) + end end context 'attributes visibility' do diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 7c80b5231d1..cecd4f76fc5 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -149,9 +149,58 @@ describe Service do end end - describe "Template" do + describe 'template' do let(:project) { create(:project) } + shared_examples 'retrieves service templates' do + it 'returns the available service templates' do + expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types) + end + end + + describe '.find_or_create_templates' do + it 'creates service templates' do + expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names.size) + end + + it_behaves_like 'retrieves service templates' + + context 'with all existing templates' do + before do + Service.insert_all( + Service.available_services_types.map { |type| { template: true, type: type } } + ) + end + + it 'does not create service templates' do + expect { Service.find_or_create_templates }.to change { Service.count }.by(0) + end + + it_behaves_like 'retrieves service templates' + + context 'with a previous existing service (Previous) and a new service (Asana)' do + before do + Service.insert(type: 'PreviousService', template: true) + Service.delete_by(type: 'AsanaService', template: true) + end + + it_behaves_like 'retrieves service templates' + end + end + + context 'with a few existing templates' do + before do + create(:jira_service, :template) + end + + it 'creates the rest of the service templates' do + expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names.size) + end + + it_behaves_like 'retrieves service templates' + end + end + describe '.build_from_template' do context 'when template is invalid' do it 'sets service template to inactive when template is invalid' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 0ed4dcec93e..86b68dc3ade 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -34,6 +34,7 @@ describe Ci::RetryBuildService do job_artifacts_container_scanning job_artifacts_dast job_artifacts_license_management job_artifacts_license_scanning job_artifacts_performance job_artifacts_lsif + job_artifacts_terraform job_artifacts_codequality job_artifacts_metrics scheduled_at job_variables waiting_for_resource_at job_artifacts_metrics_referee job_artifacts_network_referee job_artifacts_dotenv diff --git a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb index 81ac6bd94db..e58723324d3 100644 --- a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb @@ -26,3 +26,15 @@ shared_examples 'accepted carrierwave upload' do expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile)) end end + +# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') +# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. +# @param content_type [String] the upload file content type after cache +shared_examples 'upload with content type' do |content_type| + let(:fixture_file) { fixture_file_upload(path, content_type) } + + it 'will not change upload file content type' do + uploader.cache!(fixture_file) + expect(uploader.file.content_type).to eq(content_type) + end +end diff --git a/spec/uploaders/content_type_whitelist_spec.rb b/spec/uploaders/content_type_whitelist_spec.rb index be519ead1c8..4689f83759d 100644 --- a/spec/uploaders/content_type_whitelist_spec.rb +++ b/spec/uploaders/content_type_whitelist_spec.rb @@ -18,6 +18,7 @@ describe ContentTypeWhitelist do let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') } it_behaves_like 'accepted carrierwave upload' + it_behaves_like 'upload with content type', 'image/jpeg' end context 'upload non-whitelisted file content type' do diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index 60b5a6697b1..a03cf3b9dea 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -97,5 +97,12 @@ describe JobArtifactUploader do it_behaves_like "migrates", to_store: described_class::Store::REMOTE it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL + + # CI job artifacts usually are shown as text/plain, but they contain + # escape characters so MIME detectors usually fail to determine what + # the Content-Type is. + it 'does not set Content-Type' do + expect(uploader.file.content_type).to be_blank + end end end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100644..100755 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100644..100755 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |