Add latest changes from gitlab-org/gitlab@master

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

View File

@ -1 +1 @@
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
ab2f2386ab69575cd0a58f7279be707a17d7a6c8

View File

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

View File

@ -125,10 +125,12 @@ export default {
params: { id: filename },
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 v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
<div
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">
<gl-icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
@ -145,25 +147,28 @@ export default {
v-show="showImage"
:src="imageLink"
: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"
@load="onImageLoad"
@error="onImageError"
/>
</gl-intersection-observer>
</div>
<div class="card-footer d-flex w-100">
<div class="d-flex flex-column str-truncated-100">
<span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
<div class="card-footer gl-display-flex gl-w-full">
<div class="gl-display-flex gl-flex-direction-column str-truncated-100">
<span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
filename
}}</span>
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
<div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
<gl-icon name="comments" class="ml-1" />
<span :aria-label="notesLabel" class="ml-1">
<div
v-if="notesCount"
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 }}
</span>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 = {
FAILED: 'failed',

View File

@ -1,5 +1,5 @@
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 initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';

View File

@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load';
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', () => {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
}
if (gon?.features?.gitlabCiYmlPreview) {
createGitlabCiYmlVisualization();
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
def index
@keys = current_user.keys.order_id_desc
@key = Key.new
@ -31,6 +33,25 @@ class Profiles::KeysController < Profiles::ApplicationController
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
def key_params

View File

@ -35,6 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action only: :show do
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(:gitlab_ci_yml_preview, @project, default_enabled: false)
end
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true

View File

@ -37,12 +37,6 @@ class UsersController < ApplicationController
end
end
# 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
respond_to do |format|
format.html { render 'show' }

View File

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

View File

@ -93,7 +93,7 @@ class SnippetRepository < ApplicationRecord
end
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, max].max
end

View File

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

View File

@ -5,27 +5,34 @@ module Terraform
include UsageStatistics
include FileStoreMounter
DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze
UUID_LENGTH = 32
belongs_to :project
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 :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH },
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
default_value_for(:versioning_enabled, true)
mount_file_store_uploader StateUploader
default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
def file_store
super || StateUploader.default_store
end
def latest_file
versioning_enabled ? latest_version&.file : file
end
def local?
file_store == ObjectStorage::Store::LOCAL
end
@ -33,6 +40,17 @@ module Terraform
def locked?
self.lock_xid.present?
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

View File

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

View File

@ -46,7 +46,7 @@ module AlertManagement
def issue_summary_markdown
<<~MARKDOWN.chomp
#{metadata_list}
#{alert_details}#{metric_embed_for_alert}
#{metric_embed_for_alert}
MARKDOWN
end
@ -65,23 +65,6 @@ module AlertManagement
metadata.join(MARKDOWN_LINE_BREAK)
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
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ module Snippets
def check_branch_name_default!
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.')
end
@ -51,7 +51,7 @@ module Snippets
end
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)
if file_count > limit

View File

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

View File

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

View File

@ -1,6 +1,10 @@
- simple_viewer = blob.simple_viewer
- rich_viewer = blob.rich_viewer
- 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

View File

@ -4,8 +4,10 @@
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- if @merge_request.closed_without_fork?
.alert.alert-danger
The source project of this merge request has been removed.
.gl-alert.gl-alert-danger.gl-mb-5
= 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-body

View File

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

View File

@ -14,7 +14,7 @@ class IssuePlacementWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(issue_id, project_id = nil)
issue = Issue.id_in(issue_id).first
issue = find_issue(issue_id, project_id)
return unless issue
# Move the oldest 100 unpositioned items to the end.
@ -31,10 +31,19 @@ class IssuePlacementWorker
Issue.move_nulls_to_end(to_place)
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
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
IssueRebalancingWorker.perform_async(nil, issue.project_id)
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
IssueRebalancingWorker.perform_async(nil, project_id.presence || issue.project_id)
end
# 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ end
constraints(::Constraints::UserUrlConstrainer.new) do
# 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',
as: :user,

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
354524319f4c426328c7485619e248d00df323842873eaf7a2b3fbd2ad93048f

View File

@ -0,0 +1 @@
1f698671f226289fa1eabbb988b94ecd6486038f4922076bb981e44ee2356b25

View File

@ -0,0 +1 @@
d0ede6c4a28988494b0e18c073e56c1d985de73c443cc6b6d99e0b34a7f37642

View File

@ -15994,6 +15994,27 @@ CREATE SEQUENCE public.term_agreements_id_seq
ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id;
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 (
id bigint NOT NULL,
project_id bigint NOT NULL,
@ -16011,6 +16032,7 @@ CREATE TABLE public.terraform_states (
verification_retry_count smallint,
verification_checksum bytea,
verification_failure text,
versioning_enabled boolean DEFAULT false NOT NULL,
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.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.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
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
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_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_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
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
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
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
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;

View File

@ -9407,6 +9407,16 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
allowCollaboration: Boolean
"""
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.
"""

View File

@ -26108,6 +26108,34 @@
"isDeprecated": false,
"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",
"description": "Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured.",

View File

@ -1416,6 +1416,8 @@ Autogenerated return type of MarkAsSpamSnippet.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `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. |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |

View File

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

View File

@ -35,10 +35,10 @@ module API
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
get do
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
body state.file.read
body state.latest_file.read
status :ok
end
end
@ -52,8 +52,7 @@ module API
no_content! if data.empty?
remote_state_handler.handle_with_lock do |state|
state.file = CarrierWaveStringFile.new(data)
state.save!
state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial])
status :ok
end
end

View File

@ -50,7 +50,7 @@ module Gitlab
build_access_token_check(login, password) ||
lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
personal_access_token_check(password, project) ||
deploy_token_check(login, password, project) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
@ -189,12 +189,18 @@ module Gitlab
end
end
def personal_access_token_check(password)
def personal_access_token_check(password, project)
return unless password.present?
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))
end
end

View File

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

View File

@ -119,7 +119,7 @@ module Gitlab
override :check_single_change_access
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!
rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message

View File

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

View File

@ -17002,6 +17002,9 @@ msgstr ""
msgid "No containers available"
msgstr ""
msgid "No content to show"
msgstr ""
msgid "No contributions"
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."
msgstr ""
msgid "Visualization"
msgstr ""
msgid "Vulnerabilities"
msgstr ""

View File

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

View File

@ -5,7 +5,7 @@ module QA
module Project
module Issue
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_link
end

View File

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

View File

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

View File

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

View File

@ -20,4 +20,108 @@ RSpec.describe Profiles::KeysController do
expect(Key.last.expires_at).to be_like_time(expires_at)
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

View File

@ -114,71 +114,6 @@ RSpec.describe UsersController do
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
context 'for user' do
let(:project) { create(:project) }

View File

@ -7,6 +7,7 @@ FactoryBot.define do
sequence(:name) { |n| "state-#{n}" }
trait :with_file do
versioning_enabled { false }
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
end
@ -25,5 +26,14 @@ FactoryBot.define do
with_file
verification_failure { 'Could not calculate the checksum' }
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

View File

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

View File

@ -10,11 +10,11 @@ exports[`Design management list item component when item appears in view after i
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<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]"
>
<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
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"
src=""
/>
@ -31,13 +31,13 @@ exports[`Design management list item component with notes renders item with mult
</div>
<div
class="card-footer d-flex w-100"
class="card-footer gl-display-flex gl-w-full"
>
<div
class="d-flex flex-column str-truncated-100"
class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
class="bold str-truncated-100"
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
>
test
@ -57,17 +57,17 @@ exports[`Design management list item component with notes renders item with mult
</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
class="ml-1"
class="gl-ml-2"
name="comments"
size="16"
/>
<span
aria-label="2 comments"
class="ml-1"
class="gl-ml-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`] = `
<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]"
>
<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
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"
src=""
/>
@ -101,13 +101,13 @@ exports[`Design management list item component with notes renders item with sing
</div>
<div
class="card-footer d-flex w-100"
class="card-footer gl-display-flex gl-w-full"
>
<div
class="d-flex flex-column str-truncated-100"
class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
class="bold str-truncated-100"
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
>
test
@ -127,17 +127,17 @@ exports[`Design management list item component with notes renders item with sing
</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
class="ml-1"
class="gl-ml-2"
name="comments"
size="16"
/>
<span
aria-label="1 comment"
class="ml-1"
class="gl-ml-2"
>
1

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers';
import { formatDate } from '~/lib/utils/datetime_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 { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
import { isScopedLabel } from '~/lib/utils/common_utils';

View File

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

View File

@ -1,9 +1,9 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
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 label = {
color: '#333',
@ -19,7 +19,7 @@ describe('IssuableListRootApp', () => {
shouldShowFinishedAlert = false,
shouldShowInProgressAlert = false,
} = {}) =>
shallowMount(IssuableListRootApp, {
shallowMount(JiraIssuesListRoot, {
propsData: {
canEdit: true,
isJiraConfigured: true,

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