Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
984357420a
commit
591b0e86e3
|
@ -1 +1 @@
|
||||||
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
|
ab2f2386ab69575cd0a58f7279be707a17d7a6c8
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import json from './json';
|
|
||||||
import yaml from './yaml';
|
|
||||||
|
|
||||||
export default [json, yaml];
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default {
|
|
||||||
language: 'json',
|
|
||||||
options: {
|
|
||||||
validate: true,
|
|
||||||
enableSchemaRequest: true,
|
|
||||||
schemas: [],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
uri: 'https://json.schemastore.org/gitlab-ci',
|
|
||||||
fileMatch: ['*.gitlab-ci.yml'],
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
import gitlabCi from './gitlab_ci';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
language: 'yaml',
|
|
||||||
options: {
|
|
||||||
validate: true,
|
|
||||||
enableSchemaRequest: true,
|
|
||||||
hover: true,
|
|
||||||
completion: true,
|
|
||||||
schemas: [gitlabCi],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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}`],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
fragment IncidentFields on Issue {
|
||||||
|
severity
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
|
@ -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();
|
||||||
}
|
}
|
|
@ -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',
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import initIssuablesList from '~/issuables_list';
|
import initIssuablesList from '~/issues_list';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initIssuablesList();
|
initIssuablesList();
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 };
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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' }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add versioning support to Terraform state backend
|
||||||
|
merge_request: 35211
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add instance statistics visits to usage data
|
||||||
|
merge_request: 42211
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Make SSH keys publicly accessible
|
||||||
|
merge_request: 42288
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Pass project ID to issue placement worker
|
||||||
|
merge_request: 42091
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: allow project bot account to clone through http
|
||||||
|
merge_request: 40635
|
||||||
|
author: Philippe Vienne @PhilippeVienne
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix incident list by restricting query on FOSS
|
||||||
|
merge_request: 42301
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -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
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
354524319f4c426328c7485619e248d00df323842873eaf7a2b3fbd2ad93048f
|
|
@ -0,0 +1 @@
|
||||||
|
1f698671f226289fa1eabbb988b94ecd6486038f4922076bb981e44ee2356b25
|
|
@ -0,0 +1 @@
|
||||||
|
d0ede6c4a28988494b0e18c073e56c1d985de73c443cc6b6d99e0b34a7f37642
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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';
|
|
@ -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: () => {},
|
|
@ -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
Loading…
Reference in New Issue