Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-16 12:10:15 +00:00
parent 984357420a
commit 591b0e86e3
127 changed files with 1620 additions and 433 deletions

View File

@ -1 +1 @@
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2 ab2f2386ab69575cd0a58f7279be707a17d7a6c8

View File

@ -20,6 +20,7 @@ const Api = {
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks', forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels', projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
projectUsersPath: '/api/:version/projects/:id/users', projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',

View File

@ -125,10 +125,12 @@ export default {
params: { id: filename }, params: { id: filename },
query: $route.query, query: $route.query,
}" }"
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
> >
<div class="card-body p-0 d-flex-center overflow-hidden position-relative"> <div
<div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute"> class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
>
<div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip"> <span :title="icon.tooltip" :aria-label="icon.tooltip">
<gl-icon :name="icon.name" :size="18" :class="icon.classes" /> <gl-icon :name="icon.name" :size="18" :class="icon.classes" />
</span> </span>
@ -145,25 +147,28 @@ export default {
v-show="showImage" v-show="showImage"
:src="imageLink" :src="imageLink"
:alt="filename" :alt="filename"
class="block mx-auto mw-100 mh-100 design-img" class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image" data-qa-selector="design_image"
@load="onImageLoad" @load="onImageLoad"
@error="onImageError" @error="onImageError"
/> />
</gl-intersection-observer> </gl-intersection-observer>
</div> </div>
<div class="card-footer d-flex w-100"> <div class="card-footer gl-display-flex gl-w-full">
<div class="d-flex flex-column str-truncated-100"> <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
<span class="bold str-truncated-100" data-qa-selector="design_file_name">{{ <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
filename filename
}}</span> }}</span>
<span v-if="updatedAt" class="str-truncated-100"> <span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" /> {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span> </span>
</div> </div>
<div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary"> <div
<gl-icon name="comments" class="ml-1" /> v-if="notesCount"
<span :aria-label="notesLabel" class="ml-1"> class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
>
<gl-icon name="comments" class="gl-ml-2" />
<span :aria-label="notesLabel" class="gl-ml-2">
{{ notesCount }} {{ notesCount }}
</span> </span>
</div> </div>

View File

@ -14,7 +14,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue'; import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils'; import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getPathParent, readFileAsDataURL } from '../utils'; import { getPathParent, readFileAsDataURL, registerSchema } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser'; import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper'; import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
@ -56,6 +56,7 @@ export default {
'isEditModeActive', 'isEditModeActive',
'isCommitModeActive', 'isCommitModeActive',
'currentBranch', 'currentBranch',
'getJsonSchemaForPath',
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']), ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() { shouldHideEditor() {
@ -197,6 +198,8 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.registerSchemaForFile();
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()]) Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => { .then(() => {
this.createEditorInstance(); this.createEditorInstance();
@ -330,6 +333,10 @@ export default {
// do nothing if no image is found in the clipboard // do nothing if no image is found in the clipboard
return Promise.resolve(); return Promise.resolve();
}, },
registerSchemaForFile() {
const schema = this.getJsonSchemaForPath(this.file.path);
registerSchema(schema);
},
}, },
viewerTypes, viewerTypes,
FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_EDITOR,

View File

@ -7,10 +7,9 @@ import ModelManager from './common/model_manager';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options'; import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
import { themes } from './themes'; import { themes } from './themes';
import languages from './languages'; import languages from './languages';
import schemas from './schemas';
import keymap from './keymap.json'; import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils'; import { clearDomElement } from '~/editor/utils';
import { registerLanguages, registerSchemas } from '../utils'; import { registerLanguages } from '../utils';
function setupThemes() { function setupThemes() {
themes.forEach(theme => { themes.forEach(theme => {
@ -46,10 +45,6 @@ export default class Editor {
setupThemes(); setupThemes();
registerLanguages(...languages); registerLanguages(...languages);
if (gon.features?.schemaLinting) {
registerSchemas(...schemas);
}
this.debouncedUpdate = debounce(() => { this.debouncedUpdate = debounce(() => {
this.updateDimensions(); this.updateDimensions();
}, 200); }, 200);

View File

@ -1,4 +0,0 @@
import json from './json';
import yaml from './yaml';
export default [json, yaml];

View File

@ -1,8 +0,0 @@
export default {
language: 'json',
options: {
validate: true,
enableSchemaRequest: true,
schemas: [],
},
};

View File

@ -1,4 +0,0 @@
export default {
uri: 'https://json.schemastore.org/gitlab-ci',
fileMatch: ['*.gitlab-ci.yml'],
};

View File

@ -1,12 +0,0 @@
import gitlabCi from './gitlab_ci';
export default {
language: 'yaml',
options: {
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [gitlabCi],
},
};

View File

@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR, PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE, PERMISSION_PUSH_CODE,
} from '../constants'; } from '../constants';
import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
@ -177,3 +178,18 @@ export const getAvailableFileName = (state, getters) => path => {
export const getUrlForPath = state => path => export const getUrlForPath = state => path =>
`/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`; `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`;
export const getJsonSchemaForPath = (state, getters) => path => {
const [namespace, ...project] = state.currentProjectId.split('/');
return {
uri:
// eslint-disable-next-line no-restricted-globals
location.origin +
Api.buildUrl(Api.projectFileSchemaPath)
.replace(':namespace_path', namespace)
.replace(':project_path', project.join('/'))
.replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId)
.replace(':filename', path),
fileMatch: [`*${path}`],
};
};

View File

@ -75,17 +75,17 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf); languages.setLanguageConfiguration(languageId, def.conf);
} }
export function registerSchemas({ language, options }, ...schemas) { export function registerSchema(schema) {
schemas.forEach(schema => registerSchemas(schema)); const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
defaults.forEach(d =>
const defaults = { d.setDiagnosticsOptions({
json: languages.json.jsonDefaults, validate: true,
yaml: languages.yaml.yamlDefaults, enableSchemaRequest: true,
}; hover: true,
completion: true,
if (defaults[language]) { schemas: [schema],
defaults[language].setDiagnosticsOptions(options); }),
} );
} }
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);

View File

@ -0,0 +1,3 @@
fragment IncidentFields on Issue {
severity
}

View File

@ -1,3 +1,5 @@
#import "ee_else_ce/incidents/graphql/fragments/incident_fields.fragment.graphql"
query getIncidents( query getIncidents(
$projectPath: ID! $projectPath: ID!
$issueTypes: [IssueType!] $issueTypes: [IssueType!]
@ -39,8 +41,7 @@ query getIncidents(
webUrl webUrl
} }
} }
statusPagePublishedIncident ...IncidentFields
severity
} }
pageInfo { pageInfo {
hasNextPage hasNextPage

View File

@ -7,7 +7,7 @@ import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select'; import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select'; import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select'; import LabelsSelect from './labels_select';
import issueableEventHub from './issuables_list/eventhub'; import issueableEventHub from './issues_list/eventhub';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';

View File

@ -11,7 +11,7 @@ import {
} from '~/jira_import/utils/jira_import_utils'; } from '~/jira_import/utils/jira_import_utils';
export default { export default {
name: 'IssuableListRoot', name: 'JiraIssuesList',
components: { components: {
GlAlert, GlAlert,
GlLabel, GlLabel,

View File

@ -2,10 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssuableListRootApp from './components/issuable_list_root_app.vue'; import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
import IssuablesListApp from './components/issuables_list_app.vue'; import IssuablesListApp from './components/issuables_list_app.vue';
function mountIssuableListRootApp() { function mountJiraIssuesListApp() {
const el = document.querySelector('.js-projects-issues-root'); const el = document.querySelector('.js-projects-issues-root');
if (!el) { if (!el) {
@ -23,7 +23,7 @@ function mountIssuableListRootApp() {
el, el,
apolloProvider, apolloProvider,
render(createComponent) { render(createComponent) {
return createComponent(IssuableListRootApp, { return createComponent(JiraIssuesListRoot, {
props: { props: {
canEdit: parseBoolean(el.dataset.canEdit), canEdit: parseBoolean(el.dataset.canEdit),
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
@ -62,6 +62,6 @@ function mountIssuablesListApp() {
} }
export default function initIssuablesList() { export default function initIssuablesList() {
mountIssuableListRootApp(); mountJiraIssuesListApp();
mountIssuablesListApp(); mountIssuablesListApp();
} }

View File

@ -1,5 +1,5 @@
import { last } from 'lodash'; import { last } from 'lodash';
import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants'; import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
export const IMPORT_STATE = { export const IMPORT_STATE = {
FAILED: 'failed', FAILED: 'failed',

View File

@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import initIssuablesList from '~/issuables_list'; import initIssuablesList from '~/issues_list';
import projectSelect from '~/project_select'; import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search'; import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';

View File

@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load'; import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue'; import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
const createGitlabCiYmlVisualization = (containerId = '#js-blob-toggle-graph-preview') => {
const el = document.querySelector(containerId);
const { filename, blobData } = el?.dataset;
const nameRegexp = /\.gitlab-ci.yml/;
if (!el || !nameRegexp.test(filename)) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el,
components: {
GitlabCiYamlVisualization: () =>
import('~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue'),
},
render(createElement) {
return createElement('gitlabCiYamlVisualization', {
props: {
blobData,
},
});
},
});
};
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
initBlob(); initBlob();
@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
} }
if (gon?.features?.gitlabCiYmlPreview) {
createGitlabCiYmlVisualization();
}
}); });

View File

@ -1,4 +1,4 @@
import initIssuablesList from '~/issuables_list'; import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initIssuablesList(); initIssuablesList();

View File

@ -7,7 +7,7 @@ import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search'; import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants'; import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initIssuablesList from '~/issuables_list'; import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering'; import initManualOrdering from '~/manual_ordering';
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues'; import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';

View File

@ -1,5 +1,5 @@
import FilteredSearchServiceDesk from './filtered_search'; import FilteredSearchServiceDesk from './filtered_search';
import initIssuablesList from '~/issuables_list'; import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse( const supportBotData = JSON.parse(

View File

@ -5,6 +5,8 @@ import axios from '~/lib/utils/axios_utils';
import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store'; import PerformanceBarStore from './stores/performance_bar_store';
import initPerformanceBarLog from './performance_bar_log';
const initPerformanceBar = el => { const initPerformanceBar = el => {
const performanceBarData = el.dataset; const performanceBarData = el.dataset;
@ -128,4 +130,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
initPerformanceBarLog();
export default initPerformanceBar; export default initPerformanceBar;

View File

@ -0,0 +1,28 @@
/* eslint-disable no-console */
import { getCLS, getFID, getLCP } from 'web-vitals';
const initVitalsLog = () => {
const reportVital = data => {
console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data);
};
console.log(
`${String.fromCodePoint(
0x1f4d1,
)} To get the final web vital numbers reported you maybe need to switch away and back to the tab`,
);
getCLS(reportVital);
getFID(reportVital);
getLCP(reportVital);
};
const initPerformanceBarLog = () => {
console.log(
`%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
);
initVitalsLog();
};
export default initPerformanceBarLog;

View File

@ -0,0 +1,76 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
import jsYaml from 'js-yaml';
import PipelineGraph from './pipeline_graph.vue';
import { preparePipelineGraphData } from '../../utils';
export default {
FILE_CONTENT_SELECTOR: '#blob-content',
EMPTY_FILE_SELECTOR: '.nothing-here-block',
components: {
GlTab,
GlTabs,
PipelineGraph,
},
props: {
blobData: {
required: true,
type: String,
},
},
data() {
return {
selectedTabIndex: 0,
pipelineData: {},
};
},
computed: {
isVisualizationTab() {
return this.selectedTabIndex === 1;
},
},
async created() {
if (this.blobData) {
// The blobData in this case represents the gitlab-ci.yml data
const json = await jsYaml.load(this.blobData);
this.pipelineData = preparePipelineGraphData(json);
}
},
methods: {
// This is used because the blob page still uses haml, and we can't make
// our haml hide the unused section so we resort to a standard query here.
toggleFileContent({ isFileTab }) {
const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR);
const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR);
const elementToHide = el || emptySection;
if (!elementToHide) {
return;
}
// Checking for the current style display prevents user
// from toggling visiblity on and off when clicking on the tab
if (!isFileTab && elementToHide.style.display !== 'none') {
elementToHide.style.display = 'none';
}
if (isFileTab && elementToHide.style.display === 'none') {
elementToHide.style.display = 'block';
}
},
},
};
</script>
<template>
<div>
<div>
<gl-tabs v-model="selectedTabIndex">
<gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" />
<gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" />
</gl-tabs>
</div>
<pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" />
</div>
</template>

View File

@ -0,0 +1,24 @@
<script>
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
components: {
tooltipOnTruncate,
},
props: {
jobName: {
type: String,
required: true,
},
},
};
</script>
<template>
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill "
>
{{ jobName }}
</div>
</tooltip-on-truncate>
</template>

View File

@ -0,0 +1,57 @@
<script>
import { isEmpty } from 'lodash';
import { GlAlert } from '@gitlab/ui';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
export default {
components: {
GlAlert,
JobPill,
StagePill,
},
props: {
pipelineData: {
required: true,
type: Object,
},
},
computed: {
isPipelineDataEmpty() {
return isEmpty(this.pipelineData);
},
emptyClass() {
return !this.isPipelineDataEmpty ? 'gl-py-7' : '';
},
},
};
</script>
<template>
<div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass">
<gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
{{ __('No content to show') }}
</gl-alert>
<template v-else>
<div
v-for="(stage, index) in pipelineData.stages"
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
<div
class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
:class="{
'stage-left-rounded': index === 0,
'stage-right-rounded': index === pipelineData.stages.length - 1,
}"
>
<stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
</div>
<div
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
>
<job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" />
</div>
</div>
</template>
</div>
</template>

View File

@ -0,0 +1,35 @@
<script>
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
components: {
tooltipOnTruncate,
},
props: {
stageName: {
type: String,
required: true,
},
isEmpty: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
emptyClass() {
return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600';
},
},
};
</script>
<template>
<tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
<div
class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill"
:class="emptyClass"
>
{{ stageName }}
</div>
</tooltip-on-truncate>
</template>

View File

@ -4,3 +4,46 @@ import { SUPPORTED_FILTER_PARAMETERS } from './constants';
export const validateParams = params => { export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
}; };
/**
* This function takes a json payload that comes from a yml
* file converted to json through `jsyaml` library. Because we
* naively convert the entire yaml to json, some keys (like `includes`)
* are irrelevant to rendering the graph and must be removed. We also
* restructure the data to have the structure from an API response for the
* pipeline data.
* @param {Object} jsonData
* @returns {Array} - Array of stages containing all jobs
*/
export const preparePipelineGraphData = jsonData => {
const jsonKeys = Object.keys(jsonData);
const jobNames = jsonKeys.filter(job => jsonData[job]?.stage);
// We merge both the stages from the "stages" key in the yaml and the stage associated
// with each job to show the user both the stages they explicitly defined, and those
// that they added under jobs. We also remove duplicates.
const jobStages = jobNames.map(job => jsonData[job].stage);
const userDefinedStages = jsonData?.stages ?? [];
// The order is important here. We always show the stages in order they were
// defined in the `stages` key first, and then stages that are under the jobs.
const stages = Array.from(new Set([...userDefinedStages, ...jobStages]));
const arrayOfJobsByStage = stages.map(val => {
return jobNames.filter(job => {
return jsonData[job].stage === val;
});
});
const pipelineData = stages.map((stage, index) => {
const stageJobs = arrayOfJobsByStage[index];
return {
name: stage,
groups: stageJobs.map(job => {
return { name: job, jobs: [{ ...jsonData[job] }] };
}),
};
});
return { stages: pipelineData };
};

View File

@ -1081,3 +1081,19 @@ button.mini-pipeline-graph-dropdown-toggle {
.progress-bar.bg-primary { .progress-bar.bg-primary {
background-color: $blue-500 !important; background-color: $blue-500 !important;
} }
.pipeline-stage-pill {
width: 10rem;
}
.pipeline-job-pill {
width: 8rem;
}
.stage-left-rounded {
border-radius: 2rem 0 0 2rem;
}
.stage-right-rounded {
border-radius: 0 2rem 2rem 0;
}

View File

@ -1,8 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::InstanceStatisticsController < Admin::ApplicationController class Admin::InstanceStatisticsController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
before_action :check_feature_flag before_action :check_feature_flag
track_unique_visits :index, target_id: 'i_analytics_instance_statistics'
def index def index
end end

View File

@ -100,7 +100,7 @@ module NotesActions
# the finder. Here, we select between returning all notes since then, or a # the finder. Here, we select between returning all notes since then, or a
# page's worth of notes. # page's worth of notes.
def gather_notes def gather_notes
if Feature.enabled?(:paginated_notes, project) if Feature.enabled?(:paginated_notes, noteable.try(:resource_parent))
gather_some_notes gather_some_notes
else else
gather_all_notes gather_all_notes

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Profiles::KeysController < Profiles::ApplicationController class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
def index def index
@keys = current_user.keys.order_id_desc @keys = current_user.keys.order_id_desc
@key = Key.new @key = Key.new
@ -31,6 +33,25 @@ class Profiles::KeysController < Profiles::ApplicationController
end end
end end
# Get all keys of a user(params[:username]) in a text format
# Helpful for sysadmins to put in respective servers
def get_keys
if params[:username].present?
begin
user = UserFinder.new(params[:username]).find_by_username
if user.present?
render plain: user.all_ssh_keys.join("\n")
else
render_404
end
rescue => e
render html: e.message
end
else
render_404
end
end
private private
def key_params def key_params

View File

@ -35,6 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action only: :show do before_action only: :show do
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true) push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false)
end end
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true

View File

@ -37,12 +37,6 @@ class UsersController < ApplicationController
end end
end end
# Get all keys of a user(params[:username]) in a text format
# Helpful for sysadmins to put in respective servers
def ssh_keys
render plain: user.all_ssh_keys.join("\n")
end
def activity def activity
respond_to do |format| respond_to do |format|
format.html { render 'show' } format.html { render 'show' }

View File

@ -214,7 +214,7 @@ class Snippet < ApplicationRecord
def blobs def blobs
return [] unless repository_exists? return [] unless repository_exists?
repository.ls_files(repository.root_ref).map { |file| Blob.lazy(repository, repository.root_ref, file) } repository.ls_files(default_branch).map { |file| Blob.lazy(repository, default_branch, file) }
end end
def hook_attrs def hook_attrs
@ -309,6 +309,11 @@ class Snippet < ApplicationRecord
end end
end end
override :default_branch
def default_branch
super || 'master'
end
def repository_storage def repository_storage
snippet_repository&.shard_name || self.class.pick_repository_storage snippet_repository&.shard_name || self.class.pick_repository_storage
end end
@ -336,17 +341,17 @@ class Snippet < ApplicationRecord
def file_name_on_repo def file_name_on_repo
return if repository.empty? return if repository.empty?
list_files(repository.root_ref).first list_files(default_branch).first
end end
def list_files(ref = nil) def list_files(ref = nil)
return [] if repository.empty? return [] if repository.empty?
repository.ls_files(ref) repository.ls_files(ref || default_branch)
end end
def multiple_files? def multiple_files?
list_files(repository.root_ref).size > 1 list_files.size > 1
end end
class << self class << self

View File

@ -93,7 +93,7 @@ class SnippetRepository < ApplicationRecord
end end
def get_last_empty_file_index def get_last_empty_file_index
repository.ls_files(nil).inject(0) do |max, file| repository.ls_files(snippet.default_branch).inject(0) do |max, file|
idx = file[EMPTY_FILE_PATTERN, 1].to_i idx = file[EMPTY_FILE_PATTERN, 1].to_i
[idx, max].max [idx, max].max
end end

View File

@ -25,7 +25,7 @@ class SnippetStatistics < ApplicationRecord
def update_file_count def update_file_count
count = if snippet.repository_exists? count = if snippet.repository_exists?
repository.ls_files(repository.root_ref).size repository.ls_files(snippet.default_branch).size
else else
0 0
end end

View File

@ -5,27 +5,34 @@ module Terraform
include UsageStatistics include UsageStatistics
include FileStoreMounter include FileStoreMounter
DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32 UUID_LENGTH = 32
belongs_to :project belongs_to :project
belongs_to :locked_by_user, class_name: 'User' belongs_to :locked_by_user, class_name: 'User'
has_many :versions, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
has_one :latest_version, -> { ordered_by_version_desc }, class_name: 'Terraform::StateVersion', foreign_key: :terraform_state_id
scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
validates :project_id, presence: true validates :project_id, presence: true
validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' } format: { with: HEX_REGEXP, message: 'only allows hex characters' }
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
default_value_for(:versioning_enabled, true)
mount_file_store_uploader StateUploader mount_file_store_uploader StateUploader
default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
def file_store def file_store
super || StateUploader.default_store super || StateUploader.default_store
end end
def latest_file
versioning_enabled ? latest_version&.file : file
end
def local? def local?
file_store == ObjectStorage::Store::LOCAL file_store == ObjectStorage::Store::LOCAL
end end
@ -33,6 +40,17 @@ module Terraform
def locked? def locked?
self.lock_xid.present? self.lock_xid.present?
end end
def update_file!(data, version:)
if versioning_enabled?
new_version = versions.build(version: version)
new_version.assign_attributes(created_by_user: locked_by_user, file: data)
new_version.save!
else
self.file = data
save!
end
end
end end
end end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Terraform
class StateVersion < ApplicationRecord
include FileStoreMounter
belongs_to :terraform_state, class_name: 'Terraform::State', optional: false
belongs_to :created_by_user, class_name: 'User', optional: true
scope :ordered_by_version_desc, -> { order(version: :desc) }
default_value_for(:file_store) { VersionedStateUploader.default_store }
mount_file_store_uploader VersionedStateUploader
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
end
end

View File

@ -46,7 +46,7 @@ module AlertManagement
def issue_summary_markdown def issue_summary_markdown
<<~MARKDOWN.chomp <<~MARKDOWN.chomp
#{metadata_list} #{metadata_list}
#{alert_details}#{metric_embed_for_alert} #{metric_embed_for_alert}
MARKDOWN MARKDOWN
end end
@ -65,23 +65,6 @@ module AlertManagement
metadata.join(MARKDOWN_LINE_BREAK) metadata.join(MARKDOWN_LINE_BREAK)
end end
def alert_details
if details.present?
<<~MARKDOWN.chomp
#### Alert Details
#{details_list}
MARKDOWN
end
end
def details_list
details
.map { |label, value| list_item(label, value) }
.join(MARKDOWN_LINE_BREAK)
end
def metric_embed_for_alert def metric_embed_for_alert
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url "\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end end

View File

@ -51,18 +51,10 @@ module Projects
def issue_summary_markdown def issue_summary_markdown
<<~MARKDOWN.chomp <<~MARKDOWN.chomp
#{metadata_list} #{metadata_list}
#{alert_details}#{metric_embed_for_alert} #{metric_embed_for_alert}
MARKDOWN MARKDOWN
end end
def details_list
strong_memoize(:details_list) do
details
.map { |label, value| list_item(label, value) }
.join(MARKDOWN_LINE_BREAK)
end
end
def metric_embed_for_alert def metric_embed_for_alert
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url "\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end end
@ -111,17 +103,6 @@ module Projects
Gitlab::Utils::InlineHash.merge_keys(payload) Gitlab::Utils::InlineHash.merge_keys(payload)
end end
def alert_details
if details.present?
<<~MARKDOWN.chomp
#### Alert Details
#{details_list}
MARKDOWN
end
end
def list_item(key, value) def list_item(key, value)
"**#{key}:** #{value}".strip "**#{key}:** #{value}".strip
end end

View File

@ -21,7 +21,7 @@ module Issues
user = current_user user = current_user
issue.run_after_commit do issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id) NewIssueWorker.perform_async(issue.id, user.id)
IssuePlacementWorker.perform_async(issue.id) IssuePlacementWorker.perform_async(nil, issue.project_id)
end end
end end

View File

@ -82,7 +82,7 @@ module Snippets
def create_commit def create_commit
commit_attrs = { commit_attrs = {
branch_name: 'master', branch_name: @snippet.default_branch,
message: 'Initial commit' message: 'Initial commit'
} }

View File

@ -39,7 +39,7 @@ module Snippets
def check_branch_name_default! def check_branch_name_default!
branches = repository.branch_names branches = repository.branch_names
return if branches.first == Gitlab::Checks::SnippetCheck::DEFAULT_BRANCH return if branches.first == snippet.default_branch
raise RepositoryValidationError, _('Repository has an invalid default branch name.') raise RepositoryValidationError, _('Repository has an invalid default branch name.')
end end
@ -51,7 +51,7 @@ module Snippets
end end
def check_file_count! def check_file_count!
file_count = repository.ls_files(nil).size file_count = repository.ls_files(snippet.default_branch).size
limit = Snippet.max_file_limit(current_user) limit = Snippet.max_file_limit(current_user)
if file_count > limit if file_count > limit

View File

@ -93,7 +93,7 @@ module Snippets
raise UpdateError unless snippet.snippet_repository raise UpdateError unless snippet.snippet_repository
commit_attrs = { commit_attrs = {
branch_name: 'master', branch_name: snippet.default_branch,
message: 'Update snippet' message: 'Update snippet'
} }

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Terraform
class VersionedStateUploader < StateUploader
def filename
"#{model.version}.tfstate"
end
def store_dir
Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
end
end
end

View File

@ -1,6 +1,10 @@
- simple_viewer = blob.simple_viewer - simple_viewer = blob.simple_viewer
- rich_viewer = blob.rich_viewer - rich_viewer = blob.rich_viewer
- rich_viewer_active = rich_viewer && params[:viewer] != 'simple' - rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
- blob_data = defined?(@blob) ? @blob.data : {}
- filename = defined?(@blob) ? @blob.name : ''
#js-blob-toggle-graph-preview{ data: { blob_data: blob_data, filename: filename } }
= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active = render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active

View File

@ -4,8 +4,10 @@
- state_human_name, state_icon_name = state_name_with_icon(@merge_request) - state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- if @merge_request.closed_without_fork? - if @merge_request.closed_without_fork?
.alert.alert-danger .gl-alert.gl-alert-danger.gl-mb-5
The source project of this merge request has been removed. = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
The source project of this merge request has been removed.
.detail-page-header.border-bottom-0.pt-0.pb-0 .detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body .detail-page-header-body

View File

@ -1,4 +1,4 @@
.file-content.code.js-syntax-highlight #blob-content.file-content.code.js-syntax-highlight
.line-numbers .line-numbers
- if blob.data.present? - if blob.data.present?
- link_icon = sprite_icon('link', size: 12) - link_icon = sprite_icon('link', size: 12)

View File

@ -14,7 +14,7 @@ class IssuePlacementWorker
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(issue_id, project_id = nil) def perform(issue_id, project_id = nil)
issue = Issue.id_in(issue_id).first issue = find_issue(issue_id, project_id)
return unless issue return unless issue
# Move the oldest 100 unpositioned items to the end. # Move the oldest 100 unpositioned items to the end.
@ -31,10 +31,19 @@ class IssuePlacementWorker
Issue.move_nulls_to_end(to_place) Issue.move_nulls_to_end(to_place)
Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position)) Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position))
IssuePlacementWorker.perform_async(leftover.id) if leftover.present? IssuePlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
rescue RelativePositioning::NoSpaceLeft => e rescue RelativePositioning::NoSpaceLeft => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id) Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
IssueRebalancingWorker.perform_async(nil, issue.project_id) IssueRebalancingWorker.perform_async(nil, project_id.presence || issue.project_id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def find_issue(issue_id, project_id)
return Issue.id_in(issue_id).first if issue_id
project = Project.id_in(project_id).first
return unless project
project.issues.first
end
end end

View File

@ -0,0 +1,5 @@
---
title: Add versioning support to Terraform state backend
merge_request: 35211
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace bootstrap alerts in app/views/projects/merge_requests/_mr_title.html.haml
merge_request: 41399
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add instance statistics visits to usage data
merge_request: 42211
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Make SSH keys publicly accessible
merge_request: 42288
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Pass project ID to issue placement worker
merge_request: 42091
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: allow project bot account to clone through http
merge_request: 40635
author: Philippe Vienne @PhilippeVienne
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix incident list by restricting query on FOSS
merge_request: 42301
author:
type: fixed

View File

@ -0,0 +1,7 @@
---
name: gitlab_ci_yml_preview
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40880
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244905
group: group::ci
type: development
default_enabled: false

View File

@ -55,7 +55,7 @@ end
constraints(::Constraints::UserUrlConstrainer.new) do constraints(::Constraints::UserUrlConstrainer.new) do
# Get all keys of user # Get all keys of user
get ':username.keys', controller: :users, action: :ssh_keys, constraints: { username: Gitlab::PathRegex.root_namespace_route_regex } get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
scope(path: ':username', scope(path: ':username',
as: :user, as: :user,

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class CreateTerraformStateVersions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :terraform_state_versions, if_not_exists: true do |t|
t.references :terraform_state, index: false, null: false, foreign_key: { on_delete: :cascade }
t.references :created_by_user, foreign_key: false
t.timestamps_with_timezone null: false
t.integer :version, null: false
t.integer :file_store, limit: 2, null: false
t.text :file, null: false
t.index [:terraform_state_id, :version], unique: true, name: 'index_terraform_state_versions_on_state_id_and_version'
end
add_text_limit :terraform_state_versions, :file, 255
end
def down
drop_table :terraform_state_versions
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddVersioningEnabledToTerraformStates < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :terraform_states, :versioning_enabled, :boolean, null: false, default: false
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddUsersForeignKeyToTerraformStateVersions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :terraform_state_versions, :users, column: :created_by_user_id, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key_if_exists :terraform_state_versions, :users, column: :created_by_user_id
end
end
end

View File

@ -0,0 +1 @@
354524319f4c426328c7485619e248d00df323842873eaf7a2b3fbd2ad93048f

View File

@ -0,0 +1 @@
1f698671f226289fa1eabbb988b94ecd6486038f4922076bb981e44ee2356b25

View File

@ -0,0 +1 @@
d0ede6c4a28988494b0e18c073e56c1d985de73c443cc6b6d99e0b34a7f37642

View File

@ -15994,6 +15994,27 @@ CREATE SEQUENCE public.term_agreements_id_seq
ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id; ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id;
CREATE TABLE public.terraform_state_versions (
id bigint NOT NULL,
terraform_state_id bigint NOT NULL,
created_by_user_id bigint,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
version integer NOT NULL,
file_store smallint NOT NULL,
file text NOT NULL,
CONSTRAINT check_0824bb7bbd CHECK ((char_length(file) <= 255))
);
CREATE SEQUENCE public.terraform_state_versions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.terraform_state_versions_id_seq OWNED BY public.terraform_state_versions.id;
CREATE TABLE public.terraform_states ( CREATE TABLE public.terraform_states (
id bigint NOT NULL, id bigint NOT NULL,
project_id bigint NOT NULL, project_id bigint NOT NULL,
@ -16011,6 +16032,7 @@ CREATE TABLE public.terraform_states (
verification_retry_count smallint, verification_retry_count smallint,
verification_checksum bytea, verification_checksum bytea,
verification_failure text, verification_failure text,
versioning_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_21a47163ea CHECK ((char_length(verification_failure) <= 255)) CONSTRAINT check_21a47163ea CHECK ((char_length(verification_failure) <= 255))
); );
@ -17563,6 +17585,8 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id
ALTER TABLE ONLY public.term_agreements ALTER COLUMN id SET DEFAULT nextval('public.term_agreements_id_seq'::regclass); ALTER TABLE ONLY public.term_agreements ALTER COLUMN id SET DEFAULT nextval('public.term_agreements_id_seq'::regclass);
ALTER TABLE ONLY public.terraform_state_versions ALTER COLUMN id SET DEFAULT nextval('public.terraform_state_versions_id_seq'::regclass);
ALTER TABLE ONLY public.terraform_states ALTER COLUMN id SET DEFAULT nextval('public.terraform_states_id_seq'::regclass); ALTER TABLE ONLY public.terraform_states ALTER COLUMN id SET DEFAULT nextval('public.terraform_states_id_seq'::regclass);
ALTER TABLE ONLY public.timelogs ALTER COLUMN id SET DEFAULT nextval('public.timelogs_id_seq'::regclass); ALTER TABLE ONLY public.timelogs ALTER COLUMN id SET DEFAULT nextval('public.timelogs_id_seq'::regclass);
@ -18871,6 +18895,9 @@ ALTER TABLE ONLY public.tags
ALTER TABLE ONLY public.term_agreements ALTER TABLE ONLY public.term_agreements
ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id); ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.terraform_state_versions
ADD CONSTRAINT terraform_state_versions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.terraform_states ALTER TABLE ONLY public.terraform_states
ADD CONSTRAINT terraform_states_pkey PRIMARY KEY (id); ADD CONSTRAINT terraform_states_pkey PRIMARY KEY (id);
@ -21115,6 +21142,10 @@ CREATE INDEX index_term_agreements_on_term_id ON public.term_agreements USING bt
CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id); CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id);
CREATE INDEX index_terraform_state_versions_on_created_by_user_id ON public.terraform_state_versions USING btree (created_by_user_id);
CREATE UNIQUE INDEX index_terraform_state_versions_on_state_id_and_version ON public.terraform_state_versions USING btree (terraform_state_id, version);
CREATE INDEX index_terraform_states_on_file_store ON public.terraform_states USING btree (file_store); CREATE INDEX index_terraform_states_on_file_store ON public.terraform_states USING btree (file_store);
CREATE INDEX index_terraform_states_on_locked_by_user_id ON public.terraform_states USING btree (locked_by_user_id); CREATE INDEX index_terraform_states_on_locked_by_user_id ON public.terraform_states USING btree (locked_by_user_id);
@ -21913,6 +21944,9 @@ ALTER TABLE ONLY public.geo_event_log
ALTER TABLE ONLY public.projects ALTER TABLE ONLY public.projects
ADD CONSTRAINT fk_6e5c14658a FOREIGN KEY (pool_repository_id) REFERENCES public.pool_repositories(id) ON DELETE SET NULL; ADD CONSTRAINT fk_6e5c14658a FOREIGN KEY (pool_repository_id) REFERENCES public.pool_repositories(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.terraform_state_versions
ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.protected_branch_push_access_levels ALTER TABLE ONLY public.protected_branch_push_access_levels
ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
@ -22336,6 +22370,9 @@ ALTER TABLE ONLY public.events
ALTER TABLE ONLY public.ip_restrictions ALTER TABLE ONLY public.ip_restrictions
ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.terraform_state_versions
ADD CONSTRAINT fk_rails_04f176e239 FOREIGN KEY (terraform_state_id) REFERENCES public.terraform_states(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.ci_build_report_results ALTER TABLE ONLY public.ci_build_report_results
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;

View File

@ -9407,6 +9407,16 @@ type MergeRequest implements CurrentUserTodos & Noteable {
""" """
allowCollaboration: Boolean allowCollaboration: Boolean
"""
Number of approvals left
"""
approvalsLeft: Int
"""
Number of approvals required
"""
approvalsRequired: Int
""" """
Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.
""" """

View File

@ -26108,6 +26108,34 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "approvalsLeft",
"description": "Number of approvals left",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "approvalsRequired",
"description": "Number of approvals required",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "approved", "name": "approved",
"description": "Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.", "description": "Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.",

View File

@ -1416,6 +1416,8 @@ Autogenerated return type of MarkAsSpamSnippet.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork | | `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
| `approvalsLeft` | Int | Number of approvals left |
| `approvalsRequired` | Int | Number of approvals required |
| `approved` | Boolean! | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. | | `approved` | Boolean! | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
| `author` | User | User who created this merge request | | `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request | | `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |

View File

@ -14,11 +14,9 @@ Git repositories become larger over time. When large files are added to a Git re
- Git repository storage limits [can be reached](#storage-limits). - Git repository storage limits [can be reached](#storage-limits).
Rewriting a repository can remove unwanted history to make the repository smaller. Rewriting a repository can remove unwanted history to make the repository smaller.
[`git filter-repo`](https://github.com/newren/git-filter-repo) is a tool for quickly rewriting Git We **recommend [`git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/README.md)**
repository history, and is recommended over both: over [`git filter-branch`](https://git-scm.com/docs/git-filter-branch) and
[BFG](https://rtyley.github.io/bfg-repo-cleaner/).
- [`git filter-branch`](https://git-scm.com/docs/git-filter-branch).
- [BFG](https://rtyley.github.io/bfg-repo-cleaner/).
DANGER: **Danger:** DANGER: **Danger:**
Rewriting repository history is a destructive operation. Make sure to backup your repository before Rewriting repository history is a destructive operation. Make sure to backup your repository before

View File

@ -35,10 +35,10 @@ module API
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get do get do
remote_state_handler.find_with_lock do |state| remote_state_handler.find_with_lock do |state|
no_content! unless state.file.exists? no_content! unless state.latest_file && state.latest_file.exists?
env['api.format'] = :binary # this bypasses json serialization env['api.format'] = :binary # this bypasses json serialization
body state.file.read body state.latest_file.read
status :ok status :ok
end end
end end
@ -52,8 +52,7 @@ module API
no_content! if data.empty? no_content! if data.empty?
remote_state_handler.handle_with_lock do |state| remote_state_handler.handle_with_lock do |state|
state.file = CarrierWaveStringFile.new(data) state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial])
state.save!
status :ok status :ok
end end
end end

View File

@ -50,7 +50,7 @@ module Gitlab
build_access_token_check(login, password) || build_access_token_check(login, password) ||
lfs_token_check(login, password, project) || lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) || oauth_access_token_check(login, password) ||
personal_access_token_check(password) || personal_access_token_check(password, project) ||
deploy_token_check(login, password, project) || deploy_token_check(login, password, project) ||
user_with_password_for_git(login, password) || user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new Gitlab::Auth::Result.new
@ -189,12 +189,18 @@ module Gitlab
end end
end end
def personal_access_token_check(password) def personal_access_token_check(password, project)
return unless password.present? return unless password.present?
token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password) token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
if token && valid_scoped_token?(token, all_available_scopes) && token.user.can?(:log_in) return unless token
return if project && token.user.project_bot? && !project.bots.include?(token.user)
return unless valid_scoped_token?(token, all_available_scopes)
if token.user.project_bot? || token.user.can?(:log_in)
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end end
end end

View File

@ -3,7 +3,6 @@
module Gitlab module Gitlab
module Checks module Checks
class SnippetCheck < BaseChecker class SnippetCheck < BaseChecker
DEFAULT_BRANCH = 'master'.freeze
ERROR_MESSAGES = { ERROR_MESSAGES = {
create_delete_branch: 'You can not create or delete branches.' create_delete_branch: 'You can not create or delete branches.'
}.freeze }.freeze
@ -11,17 +10,18 @@ module Gitlab
ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze
attr_reader(*ATTRIBUTES) attr_reader(*ATTRIBUTES)
def initialize(change, logger:) def initialize(change, default_branch:, logger:)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref) @branch_name = Gitlab::Git.branch_name(@ref)
@tag_name = Gitlab::Git.tag_name(@ref) @tag_name = Gitlab::Git.tag_name(@ref)
@default_branch = default_branch
@logger = logger @logger = logger
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end end
def validate! def validate!
if creation? || deletion? if !@default_branch || creation? || deletion?
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch] raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
end end
@ -31,7 +31,7 @@ module Gitlab
private private
def creation? def creation?
@branch_name != DEFAULT_BRANCH && super @branch_name != @default_branch && super
end end
end end
end end

View File

@ -119,7 +119,7 @@ module Gitlab
override :check_single_change_access override :check_single_change_access
def check_single_change_access(change, _skip_lfs_integrity_check: false) def check_single_change_access(change, _skip_lfs_integrity_check: false)
Checks::SnippetCheck.new(change, logger: logger).validate! Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate!
Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate! Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
rescue Checks::TimedLogger::TimeoutError rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message raise TimeoutError, logger.full_message

View File

@ -77,6 +77,10 @@
category: analytics category: analytics
redis_slot: analytics redis_slot: analytics
aggregation: weekly aggregation: weekly
- name: i_analytics_instance_statistics
category: analytics
redis_slot: analytics
aggregation: weekly
- name: g_edit_by_web_ide - name: g_edit_by_web_ide
category: ide_edit category: ide_edit
redis_slot: edit redis_slot: edit

View File

@ -17002,6 +17002,9 @@ msgstr ""
msgid "No containers available" msgid "No containers available"
msgstr "" msgstr ""
msgid "No content to show"
msgstr ""
msgid "No contributions" msgid "No contributions"
msgstr "" msgstr ""
@ -28083,6 +28086,9 @@ msgstr ""
msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review." msgid "VisualReviewApp|Steps 1 and 2 (and sometimes 3) are performed once by the developer before requesting feedback. Steps 3 (if necessary), 4 is performed by the reviewer each time they perform a review."
msgstr "" msgstr ""
msgid "Visualization"
msgstr ""
msgid "Vulnerabilities" msgid "Vulnerabilities"
msgstr "" msgstr ""

View File

@ -153,6 +153,7 @@
"vue-virtual-scroll-list": "^1.4.4", "vue-virtual-scroll-list": "^1.4.4",
"vuedraggable": "^2.23.0", "vuedraggable": "^2.23.0",
"vuex": "^3.5.1", "vuex": "^3.5.1",
"web-vitals": "^0.2.4",
"webpack": "^4.42.0", "webpack": "^4.42.0",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",

View File

@ -5,7 +5,7 @@ module QA
module Project module Project
module Issue module Issue
class Index < Page::Base class Index < Page::Base
view 'app/assets/javascripts/issuables_list/components/issuable.vue' do view 'app/assets/javascripts/issues_list/components/issuable.vue' do
element :issue_container element :issue_container
element :issue_link element :issue_link
end end

View File

@ -18,7 +18,6 @@ RSpec.describe Admin::CohortsController do
describe 'GET #index' do describe 'GET #index' do
it_behaves_like 'tracking unique visits', :index do it_behaves_like 'tracking unique visits', :index do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_cohorts' } let(:target_id) { 'i_analytics_cohorts' }
end end
end end

View File

@ -18,7 +18,6 @@ RSpec.describe Admin::DevOpsReportController do
end end
it_behaves_like 'tracking unique visits', :show do it_behaves_like 'tracking unique visits', :show do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_dev_ops_score' } let(:target_id) { 'i_analytics_dev_ops_score' }
end end
end end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::InstanceStatisticsController do
let(:admin) { create(:user, :admin) }
before do
sign_in(admin)
end
describe 'GET #show' do
it_behaves_like 'tracking unique visits', :index do
let(:target_id) { 'i_analytics_instance_statistics' }
end
end
end

View File

@ -20,4 +20,108 @@ RSpec.describe Profiles::KeysController do
expect(Key.last.expires_at).to be_like_time(expires_at) expect(Key.last.expires_at).to be_like_time(expires_at)
end end
end end
describe "#get_keys" do
describe "non existent user" do
it "does not generally work" do
get :get_keys, params: { username: 'not-existent' }
expect(response).not_to be_successful
end
end
describe "user with no keys" do
it "does generally work" do
get :get_keys, params: { username: user.username }
expect(response).to be_successful
end
it "renders all keys separated with a new line" do
get :get_keys, params: { username: user.username }
expect(response.body).to eq("")
end
it "responds with text/plain content type" do
get :get_keys, params: { username: user.username }
expect(response.content_type).to eq("text/plain")
end
end
describe "user with keys" do
let!(:key) { create(:key, user: user) }
let!(:another_key) { create(:another_key, user: user) }
let!(:deploy_key) { create(:deploy_key, user: user) }
describe "while signed in" do
before do
sign_in(user)
end
it "does generally work" do
get :get_keys, params: { username: user.username }
expect(response).to be_successful
end
it "renders all non deploy keys separated with a new line" do
get :get_keys, params: { username: user.username }
expect(response.body).not_to eq('')
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).not_to include(deploy_key.key)
end
it "does not render the comment of the key" do
get :get_keys, params: { username: user.username }
expect(response.body).not_to match(/dummy@gitlab.com/)
end
it "responds with text/plain content type" do
get :get_keys, params: { username: user.username }
expect(response.content_type).to eq("text/plain")
end
end
describe 'when logged out' do
before do
sign_out(user)
end
it "still does generally work" do
get :get_keys, params: { username: user.username }
expect(response).to be_successful
end
it "renders all non deploy keys separated with a new line" do
get :get_keys, params: { username: user.username }
expect(response.body).not_to eq('')
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).not_to include(deploy_key.key)
end
it "does not render the comment of the key" do
get :get_keys, params: { username: user.username }
expect(response.body).not_to match(/dummy@gitlab.com/)
end
it "responds with text/plain content type" do
get :get_keys, params: { username: user.username }
expect(response.content_type).to eq("text/plain")
end
end
end
end
end end

View File

@ -114,71 +114,6 @@ RSpec.describe UsersController do
end end
end end
describe "#ssh_keys" do
describe "non existent user" do
it "does not generally work" do
get :ssh_keys, params: { username: 'not-existent' }
expect(response).not_to be_successful
end
end
describe "user with no keys" do
it "does generally work" do
get :ssh_keys, params: { username: user.username }
expect(response).to be_successful
end
it "renders all keys separated with a new line" do
get :ssh_keys, params: { username: user.username }
expect(response.body).to eq("")
end
it "responds with text/plain content type" do
get :ssh_keys, params: { username: user.username }
expect(response.content_type).to eq("text/plain")
end
end
describe "user with keys" do
let!(:key) { create(:key, user: user) }
let!(:another_key) { create(:another_key, user: user) }
let!(:deploy_key) { create(:deploy_key, user: user) }
it "does generally work" do
get :ssh_keys, params: { username: user.username }
expect(response).to be_successful
end
it "renders all non deploy keys separated with a new line" do
get :ssh_keys, params: { username: user.username }
expect(response.body).not_to eq('')
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).not_to include(deploy_key.key)
end
it "does not render the comment of the key" do
get :ssh_keys, params: { username: user.username }
expect(response.body).not_to match(/dummy@gitlab.com/)
end
it "responds with text/plain content type" do
get :ssh_keys, params: { username: user.username }
expect(response.content_type).to eq("text/plain")
end
end
end
describe 'GET #calendar' do describe 'GET #calendar' do
context 'for user' do context 'for user' do
let(:project) { create(:project) } let(:project) { create(:project) }

View File

@ -7,6 +7,7 @@ FactoryBot.define do
sequence(:name) { |n| "state-#{n}" } sequence(:name) { |n| "state-#{n}" }
trait :with_file do trait :with_file do
versioning_enabled { false }
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') } file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
end end
@ -25,5 +26,14 @@ FactoryBot.define do
with_file with_file
verification_failure { 'Could not calculate the checksum' } verification_failure { 'Could not calculate the checksum' }
end end
trait :with_version do
after(:create) do |state|
create(:terraform_state_version, :with_file, terraform_state: state)
end
end
# Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/235108
factory :legacy_terraform_state, parent: :terraform_state, traits: [:with_file]
end end
end end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :terraform_state_version, class: 'Terraform::StateVersion' do
terraform_state factory: :terraform_state
created_by_user factory: :user
sequence(:version)
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
end
end

View File

@ -10,11 +10,11 @@ exports[`Design management list item component when item appears in view after i
exports[`Design management list item component with notes renders item with multiple comments 1`] = ` exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<router-link-stub <router-link-stub
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]" to="[object Object]"
> >
<div <div
class="card-body p-0 d-flex-center overflow-hidden position-relative" class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
> >
<!----> <!---->
@ -23,7 +23,7 @@ exports[`Design management list item component with notes renders item with mult
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image" data-qa-selector="design_image"
src="" src=""
/> />
@ -31,13 +31,13 @@ exports[`Design management list item component with notes renders item with mult
</div> </div>
<div <div
class="card-footer d-flex w-100" class="card-footer gl-display-flex gl-w-full"
> >
<div <div
class="d-flex flex-column str-truncated-100" class="gl-display-flex gl-flex-direction-column str-truncated-100"
> >
<span <span
class="bold str-truncated-100" class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name" data-qa-selector="design_file_name"
> >
test test
@ -57,17 +57,17 @@ exports[`Design management list item component with notes renders item with mult
</div> </div>
<div <div
class="ml-auto d-flex align-items-center text-secondary" class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
> >
<gl-icon-stub <gl-icon-stub
class="ml-1" class="gl-ml-2"
name="comments" name="comments"
size="16" size="16"
/> />
<span <span
aria-label="2 comments" aria-label="2 comments"
class="ml-1" class="gl-ml-2"
> >
2 2
@ -80,11 +80,11 @@ exports[`Design management list item component with notes renders item with mult
exports[`Design management list item component with notes renders item with single comment 1`] = ` exports[`Design management list item component with notes renders item with single comment 1`] = `
<router-link-stub <router-link-stub
class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]" to="[object Object]"
> >
<div <div
class="card-body p-0 d-flex-center overflow-hidden position-relative" class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
> >
<!----> <!---->
@ -93,7 +93,7 @@ exports[`Design management list item component with notes renders item with sing
<img <img
alt="test" alt="test"
class="block mx-auto mw-100 mh-100 design-img" class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image" data-qa-selector="design_image"
src="" src=""
/> />
@ -101,13 +101,13 @@ exports[`Design management list item component with notes renders item with sing
</div> </div>
<div <div
class="card-footer d-flex w-100" class="card-footer gl-display-flex gl-w-full"
> >
<div <div
class="d-flex flex-column str-truncated-100" class="gl-display-flex gl-flex-direction-column str-truncated-100"
> >
<span <span
class="bold str-truncated-100" class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name" data-qa-selector="design_file_name"
> >
test test
@ -127,17 +127,17 @@ exports[`Design management list item component with notes renders item with sing
</div> </div>
<div <div
class="ml-auto d-flex align-items-center text-secondary" class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
> >
<gl-icon-stub <gl-icon-stub
class="ml-1" class="gl-ml-2"
name="comments" name="comments"
size="16" size="16"
/> />
<span <span
aria-label="1 comment" aria-label="1 comment"
class="ml-1" class="gl-ml-2"
> >
1 1

View File

@ -202,28 +202,6 @@ describe('Multi-file editor library', () => {
}); });
}); });
describe('schemas', () => {
let originalGon;
beforeEach(() => {
originalGon = window.gon;
window.gon = { features: { schemaLinting: true } };
delete Editor.editorInstance;
instance = Editor.create();
});
afterEach(() => {
window.gon = originalGon;
});
it('registers custom schemas defined with Monaco', () => {
expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
});
});
});
describe('replaceSelectedText', () => { describe('replaceSelectedText', () => {
let model; let model;
let editor; let editor;

View File

@ -1,3 +1,4 @@
import { TEST_HOST } from 'helpers/test_constants';
import * as getters from '~/ide/stores/getters'; import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { file } from '../helpers'; import { file } from '../helpers';
@ -493,4 +494,37 @@ describe('IDE store getters', () => {
); );
}); });
}); });
describe('getJsonSchemaForPath', () => {
beforeEach(() => {
localState.currentProjectId = 'path/to/some/project';
localState.currentBranchId = 'master';
});
it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => {
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
fileMatch: ['*.gitlab-ci.yml'],
uri: `${TEST_HOST}/path/to/some/project/-/schema/master/.gitlab-ci.yml`,
});
});
it('returns a path containing sha if branch details are present in state', () => {
localState.projects['path/to/some/project'] = {
name: 'project',
branches: {
master: {
name: 'master',
commit: {
id: 'abcdef123456',
},
},
},
};
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
fileMatch: ['*.gitlab-ci.yml'],
uri: `${TEST_HOST}/path/to/some/project/-/schema/abcdef123456/.gitlab-ci.yml`,
});
});
});
}); });

View File

@ -2,7 +2,7 @@ import { languages } from 'monaco-editor';
import { import {
isTextFile, isTextFile,
registerLanguages, registerLanguages,
registerSchemas, registerSchema,
trimPathComponents, trimPathComponents,
insertFinalNewline, insertFinalNewline,
trimTrailingWhitespace, trimTrailingWhitespace,
@ -159,55 +159,37 @@ describe('WebIDE utils', () => {
}); });
}); });
describe('registerSchemas', () => { describe('registerSchema', () => {
let options; let schema;
beforeEach(() => { beforeEach(() => {
options = { schema = {
validate: true, uri: 'http://myserver/foo-schema.json',
enableSchemaRequest: true, fileMatch: ['*'],
hover: true, schema: {
completion: true, id: 'http://myserver/foo-schema.json',
schemas: [ type: 'object',
{ properties: {
uri: 'http://myserver/foo-schema.json', p1: { enum: ['v1', 'v2'] },
fileMatch: ['*'], p2: { $ref: 'http://myserver/bar-schema.json' },
schema: {
id: 'http://myserver/foo-schema.json',
type: 'object',
properties: {
p1: { enum: ['v1', 'v2'] },
p2: { $ref: 'http://myserver/bar-schema.json' },
},
},
}, },
{ },
uri: 'http://myserver/bar-schema.json',
schema: {
id: 'http://myserver/bar-schema.json',
type: 'object',
properties: { q1: { enum: ['x1', 'x2'] } },
},
},
],
}; };
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions'); jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions'); jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
}); });
it.each` it('registers the given schemas with monaco for both json and yaml languages', () => {
language | defaultsObj registerSchema(schema);
${'json'} | ${languages.json.jsonDefaults}
${'yaml'} | ${languages.yaml.yamlDefaults}
`(
'registers the given schemas with monaco for lang: $language',
({ language, defaultsObj }) => {
registerSchemas({ language, options });
expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options); expect(languages.json.jsonDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
}, expect.objectContaining({ schemas: [schema] }),
); );
expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining({ schemas: [schema] }),
);
});
}); });
describe('trimTrailingWhitespace', () => { describe('trimTrailingWhitespace', () => {

View File

@ -5,7 +5,7 @@ import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import Issuable from '~/issuables_list/components/issuable.vue'; import Issuable from '~/issues_list/components/issuable.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data'; import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';

View File

@ -9,14 +9,14 @@ import {
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as flash } from '~/flash'; import { deprecatedCreateFlash as flash } from '~/flash';
import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue'; import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
import Issuable from '~/issuables_list/components/issuable.vue'; import Issuable from '~/issues_list/components/issuable.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import issueablesEventBus from '~/issuables_list/eventhub'; import issueablesEventBus from '~/issues_list/eventhub';
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants'; import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/issuables_list/eventhub'); jest.mock('~/issues_list/eventhub');
jest.mock('~/lib/utils/common_utils', () => ({ jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'), ...jest.requireActual('~/lib/utils/common_utils'),
scrollToElement: () => {}, scrollToElement: () => {},

View File

@ -1,9 +1,9 @@
import { GlAlert, GlLabel } from '@gitlab/ui'; import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue'; import JiraIssuesListRoot from '~/issues_list/components/jira_issues_list_root.vue';
describe('IssuableListRootApp', () => { describe('JiraIssuesListRoot', () => {
const issuesPath = 'gitlab-org/gitlab-test/-/issues'; const issuesPath = 'gitlab-org/gitlab-test/-/issues';
const label = { const label = {
color: '#333', color: '#333',
@ -19,7 +19,7 @@ describe('IssuableListRootApp', () => {
shouldShowFinishedAlert = false, shouldShowFinishedAlert = false,
shouldShowInProgressAlert = false, shouldShowInProgressAlert = false,
} = {}) => } = {}) =>
shallowMount(IssuableListRootApp, { shallowMount(JiraIssuesListRoot, {
propsData: { propsData: {
canEdit: true, canEdit: true,
isJiraConfigured: true, isJiraConfigured: true,

Some files were not shown because too many files have changed in this diff Show More