Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c15582526d
commit
c86ec1d072
|
@ -717,7 +717,6 @@ Gitlab/RailsLogger:
|
||||||
- 'spec/**/*.rb'
|
- 'spec/**/*.rb'
|
||||||
- 'ee/spec/**/*.rb'
|
- 'ee/spec/**/*.rb'
|
||||||
|
|
||||||
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/267606
|
|
||||||
RSpec/FactoryBot/InlineAssociation:
|
RSpec/FactoryBot/InlineAssociation:
|
||||||
Include:
|
Include:
|
||||||
- 'spec/factories/**/*.rb'
|
- 'spec/factories/**/*.rb'
|
||||||
|
|
|
@ -53,9 +53,10 @@ export const initCopyCodeButton = (selector = '#content-body') => {
|
||||||
customElements.define('copy-code', CopyCodeButton);
|
customElements.define('copy-code', CopyCodeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exclude = document.querySelector('.file-content.code'); // this behavior is not needed when viewing raw file content, so excluding it as the unnecessary dom lookups can become expensive
|
||||||
const el = document.querySelector(selector);
|
const el = document.querySelector(selector);
|
||||||
|
|
||||||
if (!el) return () => {};
|
if (!el || exclude) return () => {};
|
||||||
|
|
||||||
const observer = new MutationObserver(() => addCodeButton());
|
const observer = new MutationObserver(() => addCodeButton());
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
function addBlameLink(containerSelector, linkClass) {
|
import { getPageParamValue, getPageSearchString } from './utils';
|
||||||
|
|
||||||
|
export function addBlameLink(containerSelector, linkClass) {
|
||||||
const containerEl = document.querySelector(containerSelector);
|
const containerEl = document.querySelector(containerSelector);
|
||||||
|
|
||||||
if (!containerEl) {
|
if (!containerEl) {
|
||||||
|
@ -13,10 +15,14 @@ function addBlameLink(containerSelector, linkClass) {
|
||||||
lineLinkCopy.classList.remove(linkClass, 'diff-line-num');
|
lineLinkCopy.classList.remove(linkClass, 'diff-line-num');
|
||||||
|
|
||||||
const { lineNumber } = lineLink.dataset;
|
const { lineNumber } = lineLink.dataset;
|
||||||
const { blamePath } = document.querySelector('.line-numbers').dataset;
|
|
||||||
const blameLink = document.createElement('a');
|
const blameLink = document.createElement('a');
|
||||||
|
const { blamePath } = document.querySelector('.line-numbers').dataset;
|
||||||
blameLink.classList.add('file-line-blame');
|
blameLink.classList.add('file-line-blame');
|
||||||
blameLink.href = `${blamePath}#L${lineNumber}`;
|
const blamePerPage = document.querySelector('.js-per-page')?.dataset?.blamePerPage;
|
||||||
|
const pageNumber = getPageParamValue(lineNumber, blamePerPage);
|
||||||
|
const searchString = getPageSearchString(blamePath, pageNumber);
|
||||||
|
|
||||||
|
blameLink.href = `${blamePath}${searchString}#L${lineNumber}`;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.classList.add('line-links', 'diff-line-num');
|
wrapper.classList.add('line-links', 'diff-line-num');
|
||||||
|
@ -27,5 +33,3 @@ function addBlameLink(containerSelector, linkClass) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addBlameLink;
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getLocationHash } from '../lib/utils/url_utility';
|
import { getLocationHash } from '../lib/utils/url_utility';
|
||||||
|
import { getPageParamValue, getPageSearchString } from './utils';
|
||||||
|
|
||||||
const lineNumberRe = /^(L|LC)[0-9]+/;
|
const lineNumberRe = /^(L|LC)[0-9]+/;
|
||||||
|
|
||||||
|
@ -16,7 +17,10 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
|
||||||
permalinkButton.dataset.originalHref = href;
|
permalinkButton.dataset.originalHref = href;
|
||||||
return href;
|
return href;
|
||||||
})();
|
})();
|
||||||
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
|
const lineNum = parseInt(hash.split('L')[1], 10);
|
||||||
|
const page = getPageParamValue(lineNum);
|
||||||
|
const searchString = getPageSearchString(baseHref, page);
|
||||||
|
permalinkButton.setAttribute('href', `${baseHref}${searchString}${hashUrlString}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Editor from '~/editor/source_editor';
|
import Editor from '~/editor/source_editor';
|
||||||
|
import { getBaseURL } from '~/lib/utils/url_utility';
|
||||||
|
|
||||||
export function initSourceEditor({ el, ...args }) {
|
export function initSourceEditor({ el, ...args }) {
|
||||||
const editor = new Editor({
|
const editor = new Editor({
|
||||||
|
@ -13,4 +14,19 @@ export function initSourceEditor({ el, ...args }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blameLinesPerPage = document.querySelector('.js-per-page')?.dataset?.blamePerPage;
|
||||||
|
|
||||||
|
export const getPageParamValue = (lineNum, blamePerPage = blameLinesPerPage) => {
|
||||||
|
if (!blamePerPage) return '';
|
||||||
|
const page = Math.ceil(parseInt(lineNum, 10) / parseInt(blamePerPage, 10));
|
||||||
|
return page <= 1 ? '' : page;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPageSearchString = (path, page) => {
|
||||||
|
if (!page) return '';
|
||||||
|
const url = new URL(path, getBaseURL());
|
||||||
|
url.searchParams.set('page', page);
|
||||||
|
return url.search;
|
||||||
|
};
|
||||||
|
|
||||||
export default () => ({});
|
export default () => ({});
|
||||||
|
|
|
@ -14,7 +14,7 @@ import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
|
||||||
import CodeIntelligence from '~/code_navigation/components/app.vue';
|
import CodeIntelligence from '~/code_navigation/components/app.vue';
|
||||||
import LineHighlighter from '~/blob/line_highlighter';
|
import LineHighlighter from '~/blob/line_highlighter';
|
||||||
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
|
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
|
||||||
import addBlameLink from '~/blob/blob_blame_link';
|
import { addBlameLink } from '~/blob/blob_blame_link';
|
||||||
import projectInfoQuery from '../queries/project_info.query.graphql';
|
import projectInfoQuery from '../queries/project_info.query.graphql';
|
||||||
import getRefMixin from '../mixins/get_ref';
|
import getRefMixin from '../mixins/get_ref';
|
||||||
import userInfoQuery from '../queries/user_info.query.graphql';
|
import userInfoQuery from '../queries/user_info.query.graphql';
|
||||||
|
|
|
@ -15,8 +15,14 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
handleBlobRichViewer(this.$refs.content, this.type);
|
handleBlobRichViewer(this.$refs.content, this.type);
|
||||||
},
|
},
|
||||||
|
safeHtmlConfig: {
|
||||||
|
ADD_TAGS: ['copy-code'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<markdown-field-view ref="content" v-safe-html="richViewer || content" />
|
<markdown-field-view
|
||||||
|
ref="content"
|
||||||
|
v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
|
import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||||
|
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||||
import ChunkLine from './chunk_line.vue';
|
import ChunkLine from './chunk_line.vue';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -46,6 +47,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
totalChunks: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
language: {
|
language: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -56,11 +62,27 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lines() {
|
lines() {
|
||||||
return this.content.split('\n');
|
return this.content.split('\n');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
const { hash } = this.$route;
|
||||||
|
if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) {
|
||||||
|
// when the last chunk is loaded scroll to the hash
|
||||||
|
scrollToElement(hash, { behavior: 'auto' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleChunkAppear() {
|
handleChunkAppear() {
|
||||||
if (!this.isHighlighted) {
|
if (!this.isHighlighted) {
|
||||||
|
@ -85,17 +107,18 @@ export default {
|
||||||
:blame-path="blamePath"
|
:blame-path="blamePath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="gl-display-flex gl-text-transparent">
|
<div v-else-if="!isLoading" class="gl-display-flex gl-text-transparent">
|
||||||
<div class="gl-display-flex gl-flex-direction-column content-visibility-auto">
|
<div class="gl-display-flex gl-flex-direction-column content-visibility-auto">
|
||||||
<span
|
<span
|
||||||
v-for="(n, index) in totalLines"
|
v-for="(n, index) in totalLines"
|
||||||
|
v-once
|
||||||
:id="`L${calculateLineNumber(index)}`"
|
:id="`L${calculateLineNumber(index)}`"
|
||||||
:key="index"
|
:key="index"
|
||||||
data-testid="line-number"
|
data-testid="line-number"
|
||||||
v-text="calculateLineNumber(index)"
|
v-text="calculateLineNumber(index)"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-white-space-pre-wrap!" data-testid="content" v-text="content"></div>
|
<div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div>
|
||||||
</div>
|
</div>
|
||||||
</gl-intersection-observer>
|
</gl-intersection-observer>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlSafeHtmlDirective } from '@gitlab/ui';
|
import { GlSafeHtmlDirective } from '@gitlab/ui';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -25,6 +26,13 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
pageSearchString() {
|
||||||
|
if (!this.glFeatures.fileLineBlame) return '';
|
||||||
|
const page = getPageParamValue(this.number);
|
||||||
|
return getPageSearchString(this.blamePath, page);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
@ -35,7 +43,7 @@ export default {
|
||||||
<a
|
<a
|
||||||
v-if="glFeatures.fileLineBlame"
|
v-if="glFeatures.fileLineBlame"
|
||||||
class="gl-user-select-none gl-shadow-none! file-line-blame"
|
class="gl-user-select-none gl-shadow-none! file-line-blame"
|
||||||
:href="`${blamePath}#L${number}`"
|
:href="`${blamePath}${pageSearchString}#L${number}`"
|
||||||
></a>
|
></a>
|
||||||
<a
|
<a
|
||||||
:id="`L${number}`"
|
:id="`L${number}`"
|
||||||
|
|
|
@ -65,6 +65,9 @@ export default {
|
||||||
!supportedLanguages.includes(this.blob.language?.toLowerCase())
|
!supportedLanguages.includes(this.blob.language?.toLowerCase())
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
totalChunks() {
|
||||||
|
return Object.keys(this.chunks).length;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
addBlobLinksTracking();
|
addBlobLinksTracking();
|
||||||
|
@ -217,6 +220,7 @@ export default {
|
||||||
:chunk-index="index"
|
:chunk-index="index"
|
||||||
:language="chunk.language"
|
:language="chunk.language"
|
||||||
:blame-path="blob.blamePath"
|
:blame-path="blob.blamePath"
|
||||||
|
:total-chunks="totalChunks"
|
||||||
@appear="highlightChunk"
|
@appear="highlightChunk"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
module Packages
|
|
||||||
module Rpm
|
|
||||||
module RepositoryMetadata
|
|
||||||
class BaseBuilder
|
|
||||||
def initialize(xml: nil, data: {})
|
|
||||||
@xml = Nokogiri::XML(xml) if xml.present?
|
|
||||||
@data = data
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
|
||||||
return build_empty_structure if xml.blank?
|
|
||||||
|
|
||||||
update_xml_document
|
|
||||||
update_package_count
|
|
||||||
xml.to_xml
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_reader :xml, :data
|
|
||||||
|
|
||||||
def build_empty_structure
|
|
||||||
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
|
||||||
xml.method_missing(self.class::ROOT_TAG, self.class::ROOT_ATTRIBUTES)
|
|
||||||
end.to_xml
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_xml_document
|
|
||||||
# Add to the root xml element a new package metadata node
|
|
||||||
xml.at(self.class::ROOT_TAG).add_child(build_new_node)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_package_count
|
|
||||||
packages_count = xml.css("//#{self.class::ROOT_TAG}/package").count
|
|
||||||
|
|
||||||
xml.at(self.class::ROOT_TAG).attributes["packages"].value = packages_count.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_new_node
|
|
||||||
raise NotImplementedError, "#{self.class} should implement #{__method__}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,14 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
module Packages
|
|
||||||
module Rpm
|
|
||||||
module RepositoryMetadata
|
|
||||||
class BuildFilelistXml < ::Packages::Rpm::RepositoryMetadata::BaseBuilder
|
|
||||||
ROOT_TAG = 'filelists'
|
|
||||||
ROOT_ATTRIBUTES = {
|
|
||||||
xmlns: 'http://linux.duke.edu/metadata/filelists',
|
|
||||||
packages: '0'
|
|
||||||
}.freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,14 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
module Packages
|
|
||||||
module Rpm
|
|
||||||
module RepositoryMetadata
|
|
||||||
class BuildOtherXml < ::Packages::Rpm::RepositoryMetadata::BaseBuilder
|
|
||||||
ROOT_TAG = 'otherdata'
|
|
||||||
ROOT_ATTRIBUTES = {
|
|
||||||
xmlns: 'http://linux.duke.edu/metadata/other',
|
|
||||||
packages: '0'
|
|
||||||
}.freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
module Packages
|
||||||
|
module Rpm
|
||||||
|
module RepositoryMetadata
|
||||||
|
class BuildOtherXmlService
|
||||||
|
ROOT_TAG = 'otherdata'
|
||||||
|
ROOT_ATTRIBUTES = {
|
||||||
|
xmlns: 'http://linux.duke.edu/metadata/other',
|
||||||
|
packages: '0'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@data = data
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
builder = Nokogiri::XML::Builder.new do |xml|
|
||||||
|
xml.package(pkgid: data[:pkgid], name: data[:name], arch: data[:arch]) do
|
||||||
|
xml.version epoch: data[:epoch], ver: data[:version], rel: data[:release]
|
||||||
|
build_changelog_nodes(xml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Nokogiri::XML(builder.to_xml).at('package')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :data
|
||||||
|
|
||||||
|
def build_changelog_nodes(xml)
|
||||||
|
data[:changelogs].each do |changelog|
|
||||||
|
xml.changelog changelog[:changelogtext], date: changelog[:changelogtime]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
module Packages
|
module Packages
|
||||||
module Rpm
|
module Rpm
|
||||||
module RepositoryMetadata
|
module RepositoryMetadata
|
||||||
class BuildPrimaryXml < ::Packages::Rpm::RepositoryMetadata::BaseBuilder
|
class BuildPrimaryXmlService
|
||||||
ROOT_TAG = 'metadata'
|
ROOT_TAG = 'metadata'
|
||||||
ROOT_ATTRIBUTES = {
|
ROOT_ATTRIBUTES = {
|
||||||
xmlns: 'http://linux.duke.edu/metadata/common',
|
xmlns: 'http://linux.duke.edu/metadata/common',
|
||||||
|
@ -11,19 +11,19 @@ module Packages
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
# Nodes that have only text without attributes
|
# Nodes that have only text without attributes
|
||||||
REQUIRED_BASE_ATTRIBUTES = %i[name arch summary description].freeze
|
BASE_ATTRIBUTES = %i[name arch summary description url packager].freeze
|
||||||
NOT_REQUIRED_BASE_ATTRIBUTES = %i[url packager].freeze
|
|
||||||
FORMAT_NODE_BASE_ATTRIBUTES = %i[license vendor group buildhost sourcerpm].freeze
|
FORMAT_NODE_BASE_ATTRIBUTES = %i[license vendor group buildhost sourcerpm].freeze
|
||||||
|
|
||||||
private
|
def initialize(data)
|
||||||
|
@data = data
|
||||||
|
end
|
||||||
|
|
||||||
def build_new_node
|
def execute
|
||||||
builder = Nokogiri::XML::Builder.new do |xml|
|
builder = Nokogiri::XML::Builder.new do |xml|
|
||||||
xml.package(type: :rpm, 'xmlns:rpm': 'http://linux.duke.edu/metadata/rpm') do
|
xml.package(type: :rpm, 'xmlns:rpm': 'http://linux.duke.edu/metadata/rpm') do
|
||||||
build_required_base_attributes(xml)
|
build_base_attributes(xml)
|
||||||
build_not_required_base_attributes(xml)
|
|
||||||
xml.version epoch: data[:epoch], ver: data[:version], rel: data[:release]
|
xml.version epoch: data[:epoch], ver: data[:version], rel: data[:release]
|
||||||
xml.checksum data[:checksum], type: 'sha256', pkgid: 'YES'
|
xml.checksum data[:pkgid], type: 'sha256', pkgid: 'YES'
|
||||||
xml.size package: data[:packagesize], installed: data[:installedsize], archive: data[:archivesize]
|
xml.size package: data[:packagesize], installed: data[:installedsize], archive: data[:archivesize]
|
||||||
xml.time file: data[:filetime], build: data[:buildtime]
|
xml.time file: data[:filetime], build: data[:buildtime]
|
||||||
xml.location href: data[:location] if data[:location].present?
|
xml.location href: data[:location] if data[:location].present?
|
||||||
|
@ -34,14 +34,12 @@ module Packages
|
||||||
Nokogiri::XML(builder.to_xml).at('package')
|
Nokogiri::XML(builder.to_xml).at('package')
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_required_base_attributes(xml)
|
private
|
||||||
REQUIRED_BASE_ATTRIBUTES.each do |attribute|
|
|
||||||
xml.method_missing(attribute, data[attribute])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_not_required_base_attributes(xml)
|
attr_reader :data
|
||||||
NOT_REQUIRED_BASE_ATTRIBUTES.each do |attribute|
|
|
||||||
|
def build_base_attributes(xml)
|
||||||
|
BASE_ATTRIBUTES.each do |attribute|
|
||||||
xml.method_missing(attribute, data[attribute]) if data[attribute].present?
|
xml.method_missing(attribute, data[attribute]) if data[attribute].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
module Packages
|
module Packages
|
||||||
module Rpm
|
module Rpm
|
||||||
module RepositoryMetadata
|
module RepositoryMetadata
|
||||||
class BuildRepomdXml
|
class BuildRepomdXmlService
|
||||||
attr_reader :data
|
attr_reader :data
|
||||||
|
|
||||||
ROOT_ATTRIBUTES = {
|
ROOT_ATTRIBUTES = {
|
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
module Packages
|
||||||
|
module Rpm
|
||||||
|
module RepositoryMetadata
|
||||||
|
class UpdateXmlService
|
||||||
|
BUILDERS = {
|
||||||
|
other: ::Packages::Rpm::RepositoryMetadata::BuildOtherXmlService,
|
||||||
|
primary: ::Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize(filename:, xml: nil, data: {})
|
||||||
|
@builder_class = BUILDERS[filename]
|
||||||
|
raise ArgumentError, "Filename must be one of: #{BUILDERS.keys.join(', ')}" if @builder_class.nil?
|
||||||
|
|
||||||
|
@xml = Nokogiri::XML(xml) if xml.present?
|
||||||
|
@data = data
|
||||||
|
@filename = filename
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return build_empty_structure if xml.blank?
|
||||||
|
|
||||||
|
remove_existing_packages
|
||||||
|
update_xml_document
|
||||||
|
update_package_count
|
||||||
|
xml.to_xml
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :xml, :data, :builder_class, :filename
|
||||||
|
|
||||||
|
def build_empty_structure
|
||||||
|
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
||||||
|
xml.method_missing(builder_class::ROOT_TAG, builder_class::ROOT_ATTRIBUTES)
|
||||||
|
end.to_xml
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_xml_document
|
||||||
|
# Add to the root xml element a new package metadata node
|
||||||
|
xml.at(builder_class::ROOT_TAG).add_child(builder_class.new(data).execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_package_count
|
||||||
|
packages_count = xml.css("//#{builder_class::ROOT_TAG}/package").count
|
||||||
|
|
||||||
|
xml.at(builder_class::ROOT_TAG).attributes["packages"].value = packages_count.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_existing_packages
|
||||||
|
case filename
|
||||||
|
when :primary
|
||||||
|
xml.search("checksum:contains('#{data[:pkgid]}')").each { _1.parent&.remove }
|
||||||
|
else
|
||||||
|
xml.search("[pkgid='#{data[:pkgid]}']").each(&:remove)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,7 +6,7 @@
|
||||||
- if readme_path = @project.repository.readme_path
|
- if readme_path = @project.repository.readme_path
|
||||||
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
|
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
|
||||||
|
|
||||||
#tree-holder.tree-holder.clearfix
|
#tree-holder.tree-holder.clearfix.js-per-page{ data: { blame_per_page: Projects::BlameService::PER_PAGE } }
|
||||||
.nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch
|
.nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch
|
||||||
= render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview
|
= render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
#js-code-owners{ data: { blob_path: blob.path, project_path: @project.full_path, branch: @ref } }
|
#js-code-owners{ data: { blob_path: blob.path, project_path: @project.full_path, branch: @ref } }
|
||||||
= render "projects/blob/auxiliary_viewer", blob: blob
|
= render "projects/blob/auxiliary_viewer", blob: blob
|
||||||
|
|
||||||
#blob-content-holder.blob-content-holder
|
#blob-content-holder.blob-content-holder.js-per-page{ data: { blame_per_page: Projects::BlameService::PER_PAGE } }
|
||||||
- if @code_navigation_path
|
- if @code_navigation_path
|
||||||
#js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } }
|
#js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } }
|
||||||
- if !expanded
|
- if !expanded
|
||||||
|
|
|
@ -27,6 +27,7 @@ options:
|
||||||
- i_package_pypi_deploy_token
|
- i_package_pypi_deploy_token
|
||||||
- i_package_rubygems_deploy_token
|
- i_package_rubygems_deploy_token
|
||||||
- i_package_terraform_module_deploy_token
|
- i_package_terraform_module_deploy_token
|
||||||
|
- i_package_rpm_deploy_token
|
||||||
distribution:
|
distribution:
|
||||||
- ee
|
- ee
|
||||||
- ce
|
- ce
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: redis_hll_counters.user_packages.i_package_rpm_user_weekly
|
||||||
|
description: A weekly count of users that have published an rpm package to the registry
|
||||||
|
product_section: ops
|
||||||
|
product_stage: package
|
||||||
|
product_group: package
|
||||||
|
product_category: package_registry
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '15.6'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97133
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- i_package_rpm_user
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: redis_hll_counters.deploy_token_packages.i_package_rpm_deploy_token_weekly
|
||||||
|
description: A weekly count of RPM packages published to the registry using a deploy token
|
||||||
|
product_section: ops
|
||||||
|
product_stage: package
|
||||||
|
product_group: package
|
||||||
|
product_category: package_registry
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '15.6'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97133
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
instrumentation_class: RedisHLLMetric
|
||||||
|
options:
|
||||||
|
events:
|
||||||
|
- i_package_rpm_deploy_token
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: counts.package_events_i_package_rpm_pull_package
|
||||||
|
description: A count of RPM packages that have been downloaded
|
||||||
|
product_section: ops
|
||||||
|
product_stage: package
|
||||||
|
product_group: package
|
||||||
|
product_category: package_registry
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '15.6'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97133
|
||||||
|
time_frame: all
|
||||||
|
data_source: redis
|
||||||
|
instrumentation_class: RedisMetric
|
||||||
|
options:
|
||||||
|
prefix: package_events
|
||||||
|
event: i_package_rpm_pull_package
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
data_category: optional
|
||||||
|
key_path: counts.package_events_i_package_rpm_push_package
|
||||||
|
description: A count of RPM packages that have been published
|
||||||
|
product_section: ops
|
||||||
|
product_stage: package
|
||||||
|
product_group: package
|
||||||
|
product_category: package_registry
|
||||||
|
value_type: number
|
||||||
|
status: active
|
||||||
|
milestone: '15.6'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97133
|
||||||
|
time_frame: all
|
||||||
|
data_source: redis
|
||||||
|
instrumentation_class: RedisMetric
|
||||||
|
options:
|
||||||
|
prefix: package_events
|
||||||
|
event: i_package_rpm_push_package
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
|
@ -174,7 +174,7 @@ For installations from source:
|
||||||
RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:lfs:migrate
|
RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:lfs:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
You can optionally track progress and verify that all packages migrated successfully using the
|
You can optionally track progress and verify that all LFS objects migrated successfully using the
|
||||||
[PostgreSQL console](https://docs.gitlab.com/omnibus/settings/database.html#connecting-to-the-bundled-postgresql-database):
|
[PostgreSQL console](https://docs.gitlab.com/omnibus/settings/database.html#connecting-to-the-bundled-postgresql-database):
|
||||||
|
|
||||||
- `sudo gitlab-rails dbconsole` for Omnibus GitLab instances.
|
- `sudo gitlab-rails dbconsole` for Omnibus GitLab instances.
|
||||||
|
|
|
@ -179,7 +179,10 @@ To set issue weight of a task:
|
||||||
|
|
||||||
## Add a task to an iteration **(PREMIUM)**
|
## Add a task to an iteration **(PREMIUM)**
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.5.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc_2`. On GitLab.com, this feature is not available. The feature is not ready for production use.
|
||||||
|
|
||||||
You can add a task to an [iteration](group/iterations/index.md).
|
You can add a task to an [iteration](group/iterations/index.md).
|
||||||
You can see the iteration title and period only when you view a task.
|
You can see the iteration title and period only when you view a task.
|
||||||
|
|
|
@ -17,7 +17,7 @@ module API
|
||||||
requires :namespace_path, type: String, desc: 'Path for the namespace that should be subscribed'
|
requires :namespace_path, type: String, desc: 'Path for the namespace that should be subscribed'
|
||||||
end
|
end
|
||||||
post do
|
post do
|
||||||
not_found! unless Feature.enabled?(:jira_connect_oauth_self_managed, current_user)
|
not_found! unless Feature.enabled?(:jira_connect_oauth, current_user)
|
||||||
|
|
||||||
jwt = Atlassian::JiraConnect::Jwt::Symmetric.new(params[:jwt])
|
jwt = Atlassian::JiraConnect::Jwt::Symmetric.new(params[:jwt])
|
||||||
installation = JiraConnectInstallation.find_by_client_key(jwt.iss_claim)
|
installation = JiraConnectInstallation.find_by_client_key(jwt.iss_claim)
|
||||||
|
|
|
@ -39,6 +39,13 @@ module API
|
||||||
requires :file_name, type: String, desc: 'RPM package file name'
|
requires :file_name, type: String, desc: 'RPM package file name'
|
||||||
end
|
end
|
||||||
get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
|
get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
|
||||||
|
track_package_event(
|
||||||
|
'pull_package',
|
||||||
|
:rpm,
|
||||||
|
category: self.class.name,
|
||||||
|
project: authorized_user_project,
|
||||||
|
namespace: authorized_user_project.namespace
|
||||||
|
)
|
||||||
not_found!
|
not_found!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,6 +57,15 @@ module API
|
||||||
bad_request!('File is too large')
|
bad_request!('File is too large')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
track_package_event(
|
||||||
|
'push_package',
|
||||||
|
:rpm,
|
||||||
|
user: current_user,
|
||||||
|
category: self.class.name,
|
||||||
|
project: authorized_user_project,
|
||||||
|
namespace: authorized_user_project.namespace
|
||||||
|
)
|
||||||
|
|
||||||
not_found!
|
not_found!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -976,6 +976,24 @@ module Gitlab
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Remove any instances of deprecated job classes lingering in queues.
|
||||||
|
#
|
||||||
|
# rubocop:disable Cop/SidekiqApiUsage
|
||||||
|
def sidekiq_remove_jobs(job_klass:)
|
||||||
|
Sidekiq::Queue.new(job_klass.queue).each do |job|
|
||||||
|
job.delete if job.klass == job_klass.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq::RetrySet.new.each do |retri|
|
||||||
|
retri.delete if retri.klass == job_klass.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq::ScheduledSet.new.each do |scheduled|
|
||||||
|
scheduled.delete if scheduled.klass == job_klass.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Cop/SidekiqApiUsage
|
||||||
|
|
||||||
def sidekiq_queue_migrate(queue_from, to:)
|
def sidekiq_queue_migrate(queue_from, to:)
|
||||||
while sidekiq_queue_length(queue_from) > 0
|
while sidekiq_queue_length(queue_from) > 0
|
||||||
Sidekiq.redis do |conn|
|
Sidekiq.redis do |conn|
|
||||||
|
|
|
@ -10,6 +10,14 @@ module Gitlab
|
||||||
|
|
||||||
RECEIVED_HEADER_REGEX = /for\s+\<(.+)\>/.freeze
|
RECEIVED_HEADER_REGEX = /for\s+\<(.+)\>/.freeze
|
||||||
|
|
||||||
|
# Errors that are purely from users and not anything we can control
|
||||||
|
USER_ERRORS = [
|
||||||
|
Gitlab::Email::AutoGeneratedEmailError, Gitlab::Email::ProjectNotFound, Gitlab::Email::EmptyEmailError,
|
||||||
|
Gitlab::Email::UserNotFoundError, Gitlab::Email::UserBlockedError, Gitlab::Email::UserNotAuthorizedError,
|
||||||
|
Gitlab::Email::NoteableNotFoundError, Gitlab::Email::InvalidAttachment, Gitlab::Email::InvalidRecordError,
|
||||||
|
Gitlab::Email::EmailTooLarge
|
||||||
|
].freeze
|
||||||
|
|
||||||
def initialize(raw)
|
def initialize(raw)
|
||||||
@raw = raw
|
@raw = raw
|
||||||
end
|
end
|
||||||
|
@ -24,6 +32,9 @@ module Gitlab
|
||||||
handler.execute.tap do
|
handler.execute.tap do
|
||||||
Gitlab::Metrics::BackgroundTransaction.current&.add_event(handler.metrics_event, handler.metrics_params)
|
Gitlab::Metrics::BackgroundTransaction.current&.add_event(handler.metrics_event, handler.metrics_params)
|
||||||
end
|
end
|
||||||
|
rescue *USER_ERRORS => e
|
||||||
|
# do not send a metric event since these are purely user errors that we can't control
|
||||||
|
raise e
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Gitlab::Metrics::BackgroundTransaction.current&.add_event('email_receiver_error', error: e.class.name)
|
Gitlab::Metrics::BackgroundTransaction.current&.add_event('email_receiver_error', error: e.class.name)
|
||||||
raise e
|
raise e
|
||||||
|
|
|
@ -55,3 +55,5 @@
|
||||||
- i_package_terraform_module_delete_package
|
- i_package_terraform_module_delete_package
|
||||||
- i_package_terraform_module_pull_package
|
- i_package_terraform_module_pull_package
|
||||||
- i_package_terraform_module_push_package
|
- i_package_terraform_module_push_package
|
||||||
|
- i_package_rpm_push_package
|
||||||
|
- i_package_rpm_pull_package
|
||||||
|
|
|
@ -79,3 +79,11 @@
|
||||||
category: user_packages
|
category: user_packages
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
redis_slot: package
|
redis_slot: package
|
||||||
|
- name: i_package_rpm_user
|
||||||
|
category: user_packages
|
||||||
|
aggregation: weekly
|
||||||
|
redis_slot: package
|
||||||
|
- name: i_package_rpm_deploy_token
|
||||||
|
category: deploy_token_packages
|
||||||
|
aggregation: weekly
|
||||||
|
redis_slot: package
|
||||||
|
|
|
@ -120,15 +120,47 @@ module UnnestedInFilters
|
||||||
# "vulnerability_reads"."vulnerability_id" DESC
|
# "vulnerability_reads"."vulnerability_id" DESC
|
||||||
# LIMIT 20
|
# LIMIT 20
|
||||||
#
|
#
|
||||||
|
# If one of the columns being used for filtering or ordering is the primary key,
|
||||||
|
# then the query will be further optimized to use an index-only scan for initial filtering
|
||||||
|
# before selecting all columns using the primary key.
|
||||||
|
#
|
||||||
|
# Using the prior query as an example, where `vulnerability_id` is the primary key,
|
||||||
|
# This will be rewritten to:
|
||||||
|
#
|
||||||
|
# SELECT
|
||||||
|
# "vulnerability_reads".*
|
||||||
|
# FROM
|
||||||
|
# "vulnerability_reads"
|
||||||
|
# WHERE
|
||||||
|
# "vulnerability_reads"."vulnerability_id"
|
||||||
|
# IN (
|
||||||
|
# SELECT
|
||||||
|
# "vulnerability_reads"."vulnerability_id"
|
||||||
|
# FROM
|
||||||
|
# unnest('{1, 4}'::smallint[]) AS "states" ("state"),
|
||||||
|
# LATERAL (
|
||||||
|
# SELECT
|
||||||
|
# "vulnerability_reads"."vulnerability_id"
|
||||||
|
# FROM
|
||||||
|
# "vulnerability_reads"
|
||||||
|
# WHERE
|
||||||
|
# (vulnerability_reads."state" = "states"."state")
|
||||||
|
# ORDER BY
|
||||||
|
# "vulnerability_reads"."severity" DESC,
|
||||||
|
# "vulnerability_reads"."vulnerability_id" DESC
|
||||||
|
# LIMIT 20
|
||||||
|
# ) AS vulnerability_reads
|
||||||
|
# )
|
||||||
|
# ORDER BY
|
||||||
|
# "vulnerability_reads"."severity" DESC,
|
||||||
|
# "vulnerability_reads"."vulnerability_id" DESC
|
||||||
|
# LIMIT 20
|
||||||
def rewrite
|
def rewrite
|
||||||
log_rewrite
|
log_rewrite
|
||||||
|
|
||||||
model.from(from)
|
return filter_query unless primary_key_present?
|
||||||
.limit(limit_value)
|
|
||||||
.order(order_values)
|
index_only_filter_query
|
||||||
.includes(relation.includes_values)
|
|
||||||
.preload(relation.preload_values)
|
|
||||||
.eager_load(relation.eager_load_values)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rewrite?
|
def rewrite?
|
||||||
|
@ -147,6 +179,23 @@ module UnnestedInFilters
|
||||||
::Gitlab::AppLogger.info(message: 'Query is being rewritten by `UnnestedInFilters`', model: model.name)
|
::Gitlab::AppLogger.info(message: 'Query is being rewritten by `UnnestedInFilters`', model: model.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_query
|
||||||
|
model.from(from).then { add_relation_defaults(_1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_only_filter_query
|
||||||
|
model.where(model.primary_key => filter_query.select(model.primary_key))
|
||||||
|
.then { add_relation_defaults(_1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_relation_defaults(new_relation)
|
||||||
|
new_relation.limit(limit_value)
|
||||||
|
.order(order_values)
|
||||||
|
.includes(relation.includes_values)
|
||||||
|
.preload(relation.preload_values)
|
||||||
|
.eager_load(relation.eager_load_values)
|
||||||
|
end
|
||||||
|
|
||||||
def from
|
def from
|
||||||
[value_tables.map(&:to_sql) + [lateral]].join(', ')
|
[value_tables.map(&:to_sql) + [lateral]].join(', ')
|
||||||
end
|
end
|
||||||
|
@ -156,9 +205,13 @@ module UnnestedInFilters
|
||||||
end
|
end
|
||||||
|
|
||||||
def join_relation
|
def join_relation
|
||||||
value_tables.reduce(unscoped_relation) do |memo, tmp_table|
|
join_relation = value_tables.reduce(unscoped_relation) do |memo, tmp_table|
|
||||||
memo.where(tmp_table.as_predicate)
|
memo.where(tmp_table.as_predicate)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
join_relation = join_relation.select(combined_attributes) if primary_key_present?
|
||||||
|
|
||||||
|
join_relation
|
||||||
end
|
end
|
||||||
|
|
||||||
def unscoped_relation
|
def unscoped_relation
|
||||||
|
@ -194,10 +247,18 @@ module UnnestedInFilters
|
||||||
indices.any? do |index|
|
indices.any? do |index|
|
||||||
(filter_attributes - Array(index.columns)).empty? && # all the filter attributes are indexed
|
(filter_attributes - Array(index.columns)).empty? && # all the filter attributes are indexed
|
||||||
index.columns.last(order_attributes.length) == order_attributes && # index can be used in sorting
|
index.columns.last(order_attributes.length) == order_attributes && # index can be used in sorting
|
||||||
(index.columns - (filter_attributes + order_attributes)).empty? # there is no other columns in the index
|
(index.columns - combined_attributes).empty? # there is no other columns in the index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def primary_key_present?
|
||||||
|
combined_attributes.include?(model.primary_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def combined_attributes
|
||||||
|
filter_attributes + order_attributes
|
||||||
|
end
|
||||||
|
|
||||||
def filter_attributes
|
def filter_attributes
|
||||||
@filter_attributes ||= where_values_hash.keys
|
@filter_attributes ||= where_values_hash.keys
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"/usr/bin/hello.sh"
|
"/usr/bin/test",
|
||||||
|
"/usr/bin/test/hello.sh"
|
||||||
],
|
],
|
||||||
"changelogs": [
|
"changelogs": [
|
||||||
{
|
{
|
||||||
"changelogtext": "First build",
|
"changelogtext": "First build",
|
||||||
"changelogtime": 1662552000
|
"changelogtime": 1662552000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changelogtext": "Next build",
|
||||||
|
"changelogtime": 1662552123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"changelogtext": "Last build",
|
||||||
|
"changelogtime": 1662552321
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
|
@ -43,5 +52,7 @@
|
||||||
"group": "Unspecified",
|
"group": "Unspecified",
|
||||||
"buildhost": "localhost",
|
"buildhost": "localhost",
|
||||||
"packager": null,
|
"packager": null,
|
||||||
"vendor": null
|
"vendor": null,
|
||||||
|
"pkgid": "qwe123wer234ert345",
|
||||||
|
"epoch": "1"
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||||
import addBlameLink from '~/blob/blob_blame_link';
|
import { addBlameLink } from '~/blob/blob_blame_link';
|
||||||
|
|
||||||
describe('Blob links', () => {
|
describe('Blob links', () => {
|
||||||
const mouseoverEvent = new MouseEvent('mouseover', {
|
const mouseoverEvent = new MouseEvent('mouseover', {
|
||||||
|
@ -10,9 +10,10 @@ describe('Blob links', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setHTMLFixture(`
|
setHTMLFixture(`
|
||||||
<div id="blob-content-holder">
|
<div id="blob-content-holder" class="js-per-page" data-blame-per-page="1000">
|
||||||
<div class="line-numbers" data-blame-path="/blamePath">
|
<div class="line-numbers" data-blame-path="/blamePath">
|
||||||
<a id="L5" href="#L5" data-line-number="5" class="file-line-num js-line-links">5</a>
|
<a id="L5" href="#L5" data-line-number="5" class="file-line-num js-line-links">5</a>
|
||||||
|
<a id="L1005" href="#L1005" data-line-number="1005" class="file-line-num js-line-links">1005</a>
|
||||||
</div>
|
</div>
|
||||||
<pre id="LC5">Line 5 content</pre>
|
<pre id="LC5">Line 5 content</pre>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,4 +45,11 @@ describe('Blob links', () => {
|
||||||
expect(lineLink).not.toBeNull();
|
expect(lineLink).not.toBeNull();
|
||||||
expect(lineLink.getAttribute('href')).toBe('#L5');
|
expect(lineLink.getAttribute('href')).toBe('#L5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds page parameter when needed', () => {
|
||||||
|
document.querySelectorAll('.file-line-num')[1].dispatchEvent(mouseoverEvent);
|
||||||
|
const blameLink = document.querySelectorAll('.file-line-blame')[1];
|
||||||
|
expect(blameLink).not.toBeNull();
|
||||||
|
expect(blameLink.getAttribute('href')).toBe('/blamePath?page=2#L1005');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,4 +41,32 @@ describe('Blob utilities', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('getPageParamValue', () => {
|
||||||
|
it('returns empty string if no perPage parameter is provided', () => {
|
||||||
|
const pageParamValue = utils.getPageParamValue(5);
|
||||||
|
expect(pageParamValue).toEqual('');
|
||||||
|
});
|
||||||
|
it('returns empty string if page is equal 1', () => {
|
||||||
|
const pageParamValue = utils.getPageParamValue(1000, 1000);
|
||||||
|
expect(pageParamValue).toEqual('');
|
||||||
|
});
|
||||||
|
it('returns correct page parameter value', () => {
|
||||||
|
const pageParamValue = utils.getPageParamValue(1001, 1000);
|
||||||
|
expect(pageParamValue).toEqual(2);
|
||||||
|
});
|
||||||
|
it('accepts strings as a parameter and returns correct result', () => {
|
||||||
|
const pageParamValue = utils.getPageParamValue('1001', '1000');
|
||||||
|
expect(pageParamValue).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getPageSearchString', () => {
|
||||||
|
it('returns empty search string if page parameter is empty value', () => {
|
||||||
|
const path = utils.getPageSearchString('/blamePath', '');
|
||||||
|
expect(path).toEqual('');
|
||||||
|
});
|
||||||
|
it('returns correct search string if value is provided', () => {
|
||||||
|
const searchString = utils.getPageSearchString('/blamePath', 3);
|
||||||
|
expect(searchString).toEqual('?page=3');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -193,16 +193,18 @@ describe('deprecatedJQueryDropdown', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not focus search input while remote task is not complete', () => {
|
it('should not focus search input while remote task is not complete', () => {
|
||||||
expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
|
expect(document.activeElement).toBeDefined();
|
||||||
|
expect(document.activeElement).not.toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
|
||||||
remoteCallback();
|
remoteCallback();
|
||||||
|
|
||||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should focus search input after remote task is complete', () => {
|
it('should focus search input after remote task is complete', () => {
|
||||||
remoteCallback();
|
remoteCallback();
|
||||||
|
|
||||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
expect(document.activeElement).toBeDefined();
|
||||||
|
expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should focus on input when opening for the second time after transition', () => {
|
it('should focus on input when opening for the second time after transition', () => {
|
||||||
|
@ -215,7 +217,8 @@ describe('deprecatedJQueryDropdown', () => {
|
||||||
test.dropdownButtonElement.click();
|
test.dropdownButtonElement.click();
|
||||||
test.dropdownContainerElement.trigger('transitionend');
|
test.dropdownContainerElement.trigger('transitionend');
|
||||||
|
|
||||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
expect(document.activeElement).toBeDefined();
|
||||||
|
expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -225,7 +228,8 @@ describe('deprecatedJQueryDropdown', () => {
|
||||||
test.dropdownButtonElement.click();
|
test.dropdownButtonElement.click();
|
||||||
test.dropdownContainerElement.trigger('transitionend');
|
test.dropdownContainerElement.trigger('transitionend');
|
||||||
|
|
||||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
expect(document.activeElement).toBeDefined();
|
||||||
|
expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { GlIntersectionObserver } from '@gitlab/ui';
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
|
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
|
||||||
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
|
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
|
||||||
|
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
|
jest.mock('~/lib/utils/common_utils');
|
||||||
|
|
||||||
const DEFAULT_PROPS = {
|
const DEFAULT_PROPS = {
|
||||||
chunkIndex: 2,
|
chunkIndex: 2,
|
||||||
|
@ -13,11 +16,17 @@ const DEFAULT_PROPS = {
|
||||||
blamePath: 'blame/file.js',
|
blamePath: 'blame/file.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hash = '#L142';
|
||||||
|
|
||||||
describe('Chunk component', () => {
|
describe('Chunk component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let idleCallbackSpy;
|
||||||
|
|
||||||
const createComponent = (props = {}) => {
|
const createComponent = (props = {}) => {
|
||||||
wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } });
|
wrapper = shallowMountExtended(Chunk, {
|
||||||
|
mocks: { $route: { hash } },
|
||||||
|
propsData: { ...DEFAULT_PROPS, ...props },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
|
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
|
||||||
|
@ -26,6 +35,7 @@ describe('Chunk component', () => {
|
||||||
const findContent = () => wrapper.findByTestId('content');
|
const findContent = () => wrapper.findByTestId('content');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
|
||||||
createComponent();
|
createComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,6 +65,14 @@ describe('Chunk component', () => {
|
||||||
expect(findChunkLines().length).toBe(0);
|
expect(findChunkLines().length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not render simplified line numbers and content if browser is not in idle state', () => {
|
||||||
|
idleCallbackSpy.mockRestore();
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findLineNumbers()).toHaveLength(0);
|
||||||
|
expect(findContent().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders simplified line numbers and content if isHighlighted is false', () => {
|
it('renders simplified line numbers and content if isHighlighted is false', () => {
|
||||||
expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
|
expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
|
||||||
|
|
||||||
|
@ -76,5 +94,14 @@ describe('Chunk component', () => {
|
||||||
blamePath: DEFAULT_PROPS.blamePath,
|
blamePath: DEFAULT_PROPS.blamePath,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not scroll to route hash if last chunk is not loaded', () => {
|
||||||
|
expect(scrollToElement).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to route hash if last chunk is loaded', () => {
|
||||||
|
createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
|
||||||
|
expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
EVENT_LABEL_VIEWER,
|
EVENT_LABEL_VIEWER,
|
||||||
EVENT_LABEL_FALLBACK,
|
EVENT_LABEL_FALLBACK,
|
||||||
ROUGE_TO_HLJS_LANGUAGE_MAP,
|
ROUGE_TO_HLJS_LANGUAGE_MAP,
|
||||||
|
LINES_PER_CHUNK,
|
||||||
} from '~/vue_shared/components/source_viewer/constants';
|
} from '~/vue_shared/components/source_viewer/constants';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import LineHighlighter from '~/blob/line_highlighter';
|
import LineHighlighter from '~/blob/line_highlighter';
|
||||||
|
@ -133,45 +134,27 @@ describe('Source Viewer component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders the first chunk', async () => {
|
it.each`
|
||||||
const firstChunk = findChunks().at(0);
|
chunkIndex | chunkContent | totalChunks
|
||||||
|
${0} | ${chunk1} | ${0}
|
||||||
|
${1} | ${chunk2} | ${3}
|
||||||
|
${2} | ${chunk3Result} | ${3}
|
||||||
|
`('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
|
||||||
|
const chunk = findChunks().at(chunkIndex);
|
||||||
|
|
||||||
expect(firstChunk.props('content')).toContain(chunk1);
|
expect(chunk.props('content')).toContain(chunkContent.trim());
|
||||||
|
|
||||||
expect(firstChunk.props()).toMatchObject({
|
expect(chunk.props()).toMatchObject({
|
||||||
totalLines: 70,
|
totalLines: LINES_PER_CHUNK,
|
||||||
startingFrom: 0,
|
startingFrom: LINES_PER_CHUNK * chunkIndex,
|
||||||
|
totalChunks,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the second chunk', async () => {
|
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
|
||||||
const secondChunk = findChunks().at(1);
|
findChunks().at(0).vm.$emit('appear');
|
||||||
|
expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
|
||||||
expect(secondChunk.props('content')).toContain(chunk2.trim());
|
|
||||||
|
|
||||||
expect(secondChunk.props()).toMatchObject({
|
|
||||||
totalLines: 70,
|
|
||||||
startingFrom: 70,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the third chunk', async () => {
|
|
||||||
const thirdChunk = findChunks().at(2);
|
|
||||||
|
|
||||||
expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
|
|
||||||
|
|
||||||
expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
|
|
||||||
|
|
||||||
expect(thirdChunk.props()).toMatchObject({
|
|
||||||
totalLines: 70,
|
|
||||||
startingFrom: 140,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
|
|
||||||
findChunks().at(0).vm.$emit('appear');
|
|
||||||
expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('LineHighlighter', () => {
|
describe('LineHighlighter', () => {
|
||||||
|
|
|
@ -1924,8 +1924,116 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:same_queue_different_worker) do
|
||||||
|
Class.new do
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'test'
|
||||||
|
|
||||||
|
def self.name
|
||||||
|
'SameQueueDifferentWorkerClass'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:unrelated_worker) do
|
||||||
|
Class.new do
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'unrelated'
|
||||||
|
|
||||||
|
def self.name
|
||||||
|
'UnrelatedWorkerClass'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_const(worker.name, worker)
|
stub_const(worker.name, worker)
|
||||||
|
stub_const(unrelated_worker.name, unrelated_worker)
|
||||||
|
stub_const(same_queue_different_worker.name, same_queue_different_worker)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sidekiq_remove_jobs', :clean_gitlab_redis_queues do
|
||||||
|
def clear_queues
|
||||||
|
Sidekiq::Queue.new('test').clear
|
||||||
|
Sidekiq::Queue.new('unrelated').clear
|
||||||
|
Sidekiq::RetrySet.new.clear
|
||||||
|
Sidekiq::ScheduledSet.new.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
clear_queues
|
||||||
|
Sidekiq::Testing.disable!(&example)
|
||||||
|
clear_queues
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes all related job instances from the job class's queue" do
|
||||||
|
worker.perform_async
|
||||||
|
same_queue_different_worker.perform_async
|
||||||
|
unrelated_worker.perform_async
|
||||||
|
|
||||||
|
queue_we_care_about = Sidekiq::Queue.new(worker.queue)
|
||||||
|
unrelated_queue = Sidekiq::Queue.new(unrelated_worker.queue)
|
||||||
|
|
||||||
|
expect(queue_we_care_about.size).to eq(2)
|
||||||
|
expect(unrelated_queue.size).to eq(1)
|
||||||
|
|
||||||
|
model.sidekiq_remove_jobs(job_klass: worker)
|
||||||
|
|
||||||
|
expect(queue_we_care_about.size).to eq(1)
|
||||||
|
expect(queue_we_care_about.map(&:klass)).not_to include(worker.name)
|
||||||
|
expect(queue_we_care_about.map(&:klass)).to include(
|
||||||
|
same_queue_different_worker.name
|
||||||
|
)
|
||||||
|
expect(unrelated_queue.size).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job instances are in the scheduled set' do
|
||||||
|
it 'removes all related job instances from the scheduled set' do
|
||||||
|
worker.perform_in(1.hour)
|
||||||
|
unrelated_worker.perform_in(1.hour)
|
||||||
|
|
||||||
|
scheduled = Sidekiq::ScheduledSet.new
|
||||||
|
|
||||||
|
expect(scheduled.size).to eq(2)
|
||||||
|
expect(scheduled.map(&:klass)).to include(
|
||||||
|
worker.name,
|
||||||
|
unrelated_worker.name
|
||||||
|
)
|
||||||
|
|
||||||
|
model.sidekiq_remove_jobs(job_klass: worker)
|
||||||
|
|
||||||
|
expect(scheduled.size).to eq(1)
|
||||||
|
expect(scheduled.map(&:klass)).not_to include(worker.name)
|
||||||
|
expect(scheduled.map(&:klass)).to include(unrelated_worker.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job instances are in the retry set' do
|
||||||
|
include_context 'when handling retried jobs'
|
||||||
|
|
||||||
|
it 'removes all related job instances from the retry set' do
|
||||||
|
retry_in(worker, 1.hour)
|
||||||
|
retry_in(worker, 2.hours)
|
||||||
|
retry_in(worker, 3.hours)
|
||||||
|
retry_in(unrelated_worker, 4.hours)
|
||||||
|
|
||||||
|
retries = Sidekiq::RetrySet.new
|
||||||
|
|
||||||
|
expect(retries.size).to eq(4)
|
||||||
|
expect(retries.map(&:klass)).to include(
|
||||||
|
worker.name,
|
||||||
|
unrelated_worker.name
|
||||||
|
)
|
||||||
|
|
||||||
|
model.sidekiq_remove_jobs(job_klass: worker)
|
||||||
|
|
||||||
|
expect(retries.size).to eq(1)
|
||||||
|
expect(retries.map(&:klass)).not_to include(worker.name)
|
||||||
|
expect(retries.map(&:klass)).to include(unrelated_worker.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#sidekiq_queue_length' do
|
describe '#sidekiq_queue_length' do
|
||||||
|
@ -1949,7 +2057,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#migrate_sidekiq_queue' do
|
describe '#sidekiq_queue_migrate' do
|
||||||
it 'migrates jobs from one sidekiq queue to another' do
|
it 'migrates jobs from one sidekiq queue to another' do
|
||||||
Sidekiq::Testing.disable! do
|
Sidekiq::Testing.disable! do
|
||||||
worker.perform_async('Something', [1])
|
worker.perform_async('Something', [1])
|
||||||
|
|
|
@ -5,11 +5,10 @@ require 'spec_helper'
|
||||||
RSpec.describe Gitlab::Email::Receiver do
|
RSpec.describe Gitlab::Email::Receiver do
|
||||||
include_context :email_shared_context
|
include_context :email_shared_context
|
||||||
|
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
let(:metric_transaction) { instance_double(Gitlab::Metrics::WebTransaction) }
|
let(:metric_transaction) { instance_double(Gitlab::Metrics::WebTransaction) }
|
||||||
|
|
||||||
shared_examples 'successful receive' do
|
shared_examples 'successful receive' do
|
||||||
let_it_be(:project) { create(:project) }
|
|
||||||
|
|
||||||
let(:handler) { double(:handler, project: project, execute: true, metrics_event: nil, metrics_params: nil) }
|
let(:handler) { double(:handler, project: project, execute: true, metrics_event: nil, metrics_params: nil) }
|
||||||
let(:client_id) { 'email/jake@example.com' }
|
let(:client_id) { 'email/jake@example.com' }
|
||||||
|
|
||||||
|
@ -39,7 +38,7 @@ RSpec.describe Gitlab::Email::Receiver do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'failed receive' do
|
shared_examples 'failed receive with event' do
|
||||||
it 'adds metric event' do
|
it 'adds metric event' do
|
||||||
expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
|
expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
|
||||||
expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name })
|
expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name })
|
||||||
|
@ -48,6 +47,14 @@ RSpec.describe Gitlab::Email::Receiver do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'failed receive without event' do
|
||||||
|
it 'adds metric event' do
|
||||||
|
expect(::Gitlab::Metrics::BackgroundTransaction).not_to receive(:current)
|
||||||
|
|
||||||
|
expect { receiver.execute }.to raise_error(expected_error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the email contains a valid email address in a header' do
|
context 'when the email contains a valid email address in a header' do
|
||||||
before do
|
before do
|
||||||
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
|
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
|
||||||
|
@ -100,21 +107,21 @@ RSpec.describe Gitlab::Email::Receiver do
|
||||||
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, '!!!') }
|
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, '!!!') }
|
||||||
let(:expected_error) { Gitlab::Email::UnknownIncomingEmail }
|
let(:expected_error) { Gitlab::Email::UnknownIncomingEmail }
|
||||||
|
|
||||||
it_behaves_like 'failed receive'
|
it_behaves_like 'failed receive with event'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the email is blank' do
|
context 'when the email is blank' do
|
||||||
let(:email_raw) { '' }
|
let(:email_raw) { '' }
|
||||||
let(:expected_error) { Gitlab::Email::EmptyEmailError }
|
let(:expected_error) { Gitlab::Email::EmptyEmailError }
|
||||||
|
|
||||||
it_behaves_like 'failed receive'
|
it_behaves_like 'failed receive without event'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the email was auto generated with Auto-Submitted header' do
|
context 'when the email was auto generated with Auto-Submitted header' do
|
||||||
let(:email_raw) { fixture_file('emails/auto_submitted.eml') }
|
let(:email_raw) { fixture_file('emails/auto_submitted.eml') }
|
||||||
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
|
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
|
||||||
|
|
||||||
it_behaves_like 'failed receive'
|
it_behaves_like 'failed receive without event'
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the email's To field is blank" do
|
context "when the email's To field is blank" do
|
||||||
|
@ -164,7 +171,48 @@ RSpec.describe Gitlab::Email::Receiver do
|
||||||
let(:email_raw) { fixture_file('emails/auto_reply.eml') }
|
let(:email_raw) { fixture_file('emails/auto_reply.eml') }
|
||||||
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
|
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
|
||||||
|
|
||||||
it_behaves_like 'failed receive'
|
it_behaves_like 'failed receive without event'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'event raising via errors' do
|
||||||
|
let(:handler) { double(:handler, project: project, execute: true, metrics_event: nil, metrics_params: nil) }
|
||||||
|
let(:email_raw) { "arbitrary text. could be anything really. we're going to raise an error anyway." }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(receiver).to receive(:handler).and_return(handler)
|
||||||
|
allow(handler).to receive(:execute).and_raise(expected_error)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'handling errors which do not raise events' do
|
||||||
|
where(:expected_error) do
|
||||||
|
[
|
||||||
|
Gitlab::Email::AutoGeneratedEmailError,
|
||||||
|
Gitlab::Email::ProjectNotFound,
|
||||||
|
Gitlab::Email::EmptyEmailError,
|
||||||
|
Gitlab::Email::UserNotFoundError,
|
||||||
|
Gitlab::Email::UserBlockedError,
|
||||||
|
Gitlab::Email::UserNotAuthorizedError,
|
||||||
|
Gitlab::Email::NoteableNotFoundError,
|
||||||
|
Gitlab::Email::InvalidAttachment,
|
||||||
|
Gitlab::Email::InvalidRecordError,
|
||||||
|
Gitlab::Email::EmailTooLarge
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it_behaves_like 'failed receive without event'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'handling errors which do raise events' do
|
||||||
|
where(:expected_error) do
|
||||||
|
[Gitlab::Email::EmailUnparsableError, Gitlab::Email::UnknownIncomingEmail, ArgumentError, StandardError]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it_behaves_like 'failed receive with event'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'requires all handlers to have a unique metric_event' do
|
it 'requires all handlers to have a unique metric_event' do
|
||||||
|
|
|
@ -208,25 +208,6 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'retried jobs' do
|
context 'retried jobs' do
|
||||||
let(:set_name) { 'retry' }
|
|
||||||
# Account for Sidekiq retry jitter
|
|
||||||
# https://github.com/mperham/sidekiq/blob/3575ccb44c688dd08bfbfd937696260b12c622fb/lib/sidekiq/job_retry.rb#L217
|
|
||||||
let(:schedule_jitter) { 10 }
|
|
||||||
|
|
||||||
# Try to mimic as closely as possible what Sidekiq will actually
|
|
||||||
# do to retry a job.
|
|
||||||
def retry_in(klass, time, args)
|
|
||||||
message = { 'class' => klass.name, 'args' => [args], 'retry' => true }.to_json
|
|
||||||
|
|
||||||
allow(klass).to receive(:sidekiq_retry_in_block).and_return(proc { time })
|
|
||||||
|
|
||||||
begin
|
|
||||||
Sidekiq::JobRetry.new.local(klass, message, klass.queue) { raise 'boom' }
|
|
||||||
rescue Sidekiq::JobRetry::Skip
|
|
||||||
# Sidekiq scheduled the retry
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_jobs(include_post_receive: true)
|
def create_jobs(include_post_receive: true)
|
||||||
retry_in(AuthorizedProjectsWorker, 1.hour, 0)
|
retry_in(AuthorizedProjectsWorker, 1.hour, 0)
|
||||||
retry_in(AuthorizedProjectsWorker, 2.hours, 1)
|
retry_in(AuthorizedProjectsWorker, 2.hours, 1)
|
||||||
|
@ -234,6 +215,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
|
||||||
retry_in(AuthorizedProjectsWorker, 4.hours, 3)
|
retry_in(AuthorizedProjectsWorker, 4.hours, 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_context 'when handling retried jobs'
|
||||||
it_behaves_like 'processing a set'
|
it_behaves_like 'processing a set'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -170,6 +170,51 @@ RSpec.describe UnnestedInFilters::Rewriter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the combined attributes include the primary key' do
|
||||||
|
let(:relation) { User.where(user_type: %i(support_bot alert_bot)).order(id: :desc).limit(2) }
|
||||||
|
|
||||||
|
let(:expected_query) do
|
||||||
|
<<~SQL
|
||||||
|
SELECT
|
||||||
|
"users".*
|
||||||
|
FROM
|
||||||
|
"users"
|
||||||
|
WHERE
|
||||||
|
"users"."id" IN (
|
||||||
|
SELECT
|
||||||
|
"users"."id"
|
||||||
|
FROM
|
||||||
|
unnest('{1,2}' :: smallint []) AS "user_types"("user_type"),
|
||||||
|
LATERAL (
|
||||||
|
SELECT
|
||||||
|
"users"."user_type",
|
||||||
|
"users"."id"
|
||||||
|
FROM
|
||||||
|
"users"
|
||||||
|
WHERE
|
||||||
|
(users."user_type" = "user_types"."user_type")
|
||||||
|
ORDER BY
|
||||||
|
"users"."id" DESC
|
||||||
|
LIMIT
|
||||||
|
2
|
||||||
|
) AS users
|
||||||
|
ORDER BY
|
||||||
|
"users"."id" DESC
|
||||||
|
LIMIT
|
||||||
|
2
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
"users"."id" DESC
|
||||||
|
LIMIT
|
||||||
|
2
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes the query' do
|
||||||
|
expect(issued_query.gsub(/\s/, '')).to start_with(expected_query.gsub(/\s/, ''))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'logging' do
|
describe 'logging' do
|
||||||
subject(:load_reload) { rewriter.rewrite }
|
subject(:load_reload) { rewriter.rewrite }
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe API::Integrations::JiraConnect::Subscriptions do
|
||||||
|
|
||||||
context 'with feature flag disabled' do
|
context 'with feature flag disabled' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(jira_connect_oauth_self_managed: false)
|
stub_feature_flags(jira_connect_oauth: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:jwt) { '123' }
|
let(:jwt) { '123' }
|
||||||
|
|
|
@ -9,7 +9,8 @@ RSpec.describe API::RpmProjectPackages do
|
||||||
|
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
let_it_be_with_reload(:project) { create(:project, :public) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
|
let_it_be_with_reload(:project) { create(:project, :public, group: group) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||||
|
@ -133,16 +134,19 @@ RSpec.describe API::RpmProjectPackages do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
|
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
|
||||||
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: group } }
|
||||||
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
|
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
|
||||||
|
|
||||||
subject { get api(url), headers: headers }
|
subject { get api(url), headers: headers }
|
||||||
|
|
||||||
|
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
|
||||||
it_behaves_like 'a job token for RPM requests'
|
it_behaves_like 'a job token for RPM requests'
|
||||||
it_behaves_like 'a deploy token for RPM requests'
|
it_behaves_like 'a deploy token for RPM requests'
|
||||||
it_behaves_like 'a user token for RPM requests'
|
it_behaves_like 'a user token for RPM requests'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
|
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
|
||||||
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, user: user } }
|
||||||
let(:url) { "/projects/#{project.id}/packages/rpm" }
|
let(:url) { "/projects/#{project.id}/packages/rpm" }
|
||||||
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
|
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
|
||||||
|
|
||||||
|
@ -150,25 +154,25 @@ RSpec.describe API::RpmProjectPackages do
|
||||||
|
|
||||||
context 'with user token' do
|
context 'with user token' do
|
||||||
context 'with valid project' do
|
context 'with valid project' do
|
||||||
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
|
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status, :tracked) do
|
||||||
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found | true
|
||||||
'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
|
'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden | false
|
||||||
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
|
'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found | false
|
||||||
'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
|
'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found | false
|
||||||
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
|
'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
|
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found | true
|
||||||
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
|
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden | false
|
||||||
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
|
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
|
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
|
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found | false
|
||||||
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
|
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found | false
|
||||||
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
|
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
|
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized | false
|
||||||
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
|
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized | false
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
@ -180,6 +184,8 @@ RSpec.describe API::RpmProjectPackages do
|
||||||
project.send("add_#{user_role}", user) if member && user_role != :anonymous
|
project.send("add_#{user_role}", user) if member && user_role != :anonymous
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tracking_example = params[:tracked] ? 'a package tracking event' : 'not a package tracking event'
|
||||||
|
it_behaves_like tracking_example, described_class.name, 'push_package'
|
||||||
it_behaves_like params[:shared_examples_name], params[:expected_status]
|
it_behaves_like params[:shared_examples_name], params[:expected_status]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,17 +56,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when an ambiguous branch name is provided' do
|
|
||||||
let(:branches) { { 'ambiguous/test' => 'master', 'ambiguous' => 'master' } }
|
|
||||||
|
|
||||||
it 'returns an error that branch could not be created' do
|
|
||||||
err_msg = 'Failed to create branch \'ambiguous\': 13:reference is ambiguous.'
|
|
||||||
|
|
||||||
expect(subject[:status]).to eq(:error)
|
|
||||||
expect(subject[:message]).to match_array([err_msg])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when PreReceiveError exception' do
|
context 'when PreReceiveError exception' do
|
||||||
let(:branches) { { 'error' => 'master' } }
|
let(:branches) { { 'error' => 'master' } }
|
||||||
|
|
||||||
|
@ -184,18 +173,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when an ambiguous branch name is provided' do
|
|
||||||
it 'returns an error that branch could not be created' do
|
|
||||||
err_msg = 'Failed to create branch \'feature\': 13:reference is ambiguous.'
|
|
||||||
|
|
||||||
service.execute('feature/widget', 'master')
|
|
||||||
result = service.execute('feature', 'master')
|
|
||||||
|
|
||||||
expect(result[:status]).to eq(:error)
|
|
||||||
expect(result[:message]).to eq(err_msg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'logs and returns an error if there is a PreReceiveError exception' do
|
it 'logs and returns an error if there is a PreReceiveError exception' do
|
||||||
error_message = 'pre receive error'
|
error_message = 'pre receive error'
|
||||||
raw_message = "GitLab: #{error_message}"
|
raw_message = "GitLab: #{error_message}"
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Packages::Rpm::RepositoryMetadata::BaseBuilder do
|
|
||||||
describe '#execute' do
|
|
||||||
subject { described_class.new(xml: xml, data: data).execute }
|
|
||||||
|
|
||||||
let(:xml) { nil }
|
|
||||||
let(:data) { {} }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_const("#{described_class}::ROOT_TAG", 'test')
|
|
||||||
stub_const("#{described_class}::ROOT_ATTRIBUTES", { foo1: 'bar1', foo2: 'bar2' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'generate valid xml' do
|
|
||||||
result = Nokogiri::XML::Document.parse(subject)
|
|
||||||
|
|
||||||
expect(result.children.count).to eq(1)
|
|
||||||
expect(result.children.first.attributes.count).to eq(2)
|
|
||||||
expect(result.children.first.attributes['foo1'].value).to eq('bar1')
|
|
||||||
expect(result.children.first.attributes['foo2'].value).to eq('bar2')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when call with parameters' do
|
|
||||||
let(:xml) { 'test' }
|
|
||||||
|
|
||||||
it 'raise NotImplementedError' do
|
|
||||||
expect { subject }.to raise_error NotImplementedError
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,21 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXml do
|
|
||||||
describe '#execute' do
|
|
||||||
subject { described_class.new.execute }
|
|
||||||
|
|
||||||
context "when generate empty xml" do
|
|
||||||
let(:expected_xml) do
|
|
||||||
<<~XML
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
|
|
||||||
XML
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'generate expected xml' do
|
|
||||||
expect(subject).to eq(expected_xml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXmlService do
|
||||||
|
describe '#execute' do
|
||||||
|
subject { described_class.new(data).execute }
|
||||||
|
|
||||||
|
context 'when updating existing xml' do
|
||||||
|
include_context 'with rpm package data'
|
||||||
|
|
||||||
|
let(:data) { xml_update_params }
|
||||||
|
let(:changelog_xpath) { "//package/changelog" }
|
||||||
|
|
||||||
|
it 'adds all changelog nodes' do
|
||||||
|
result = subject
|
||||||
|
|
||||||
|
expect(result.xpath(changelog_xpath).count).to eq(data[:changelogs].count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'set required date attribute' do
|
||||||
|
result = subject
|
||||||
|
|
||||||
|
data[:changelogs].each do |changelog|
|
||||||
|
expect(result.at("#{changelog_xpath}[@date=\"#{changelog[:changelogtime]}\"]")).not_to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,21 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXml do
|
|
||||||
describe '#execute' do
|
|
||||||
subject { described_class.new.execute }
|
|
||||||
|
|
||||||
context "when generate empty xml" do
|
|
||||||
let(:expected_xml) do
|
|
||||||
<<~XML
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
|
|
||||||
XML
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'generate expected xml' do
|
|
||||||
expect(subject).to eq(expected_xml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,32 +1,22 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXml do
|
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
subject { described_class.new(xml: xml, data: data).execute }
|
subject { described_class.new(data).execute }
|
||||||
|
|
||||||
let(:empty_xml) do
|
|
||||||
<<~XML
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
|
|
||||||
XML
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'handling rpm xml file'
|
|
||||||
|
|
||||||
context 'when updating existing xml' do
|
context 'when updating existing xml' do
|
||||||
include_context 'with rpm package data'
|
include_context 'with rpm package data'
|
||||||
|
|
||||||
let(:xml) { empty_xml }
|
|
||||||
let(:data) { xml_update_params }
|
let(:data) { xml_update_params }
|
||||||
let(:required_text_only_attributes) { %i[description summary arch name] }
|
let(:required_text_only_attributes) { %i[description summary arch name] }
|
||||||
|
|
||||||
it 'adds node with required_text_only_attributes' do
|
it 'adds node with required_text_only_attributes' do
|
||||||
result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
|
result = subject
|
||||||
|
|
||||||
required_text_only_attributes.each do |attribute|
|
required_text_only_attributes.each do |attribute|
|
||||||
expect(
|
expect(
|
||||||
result.at("//#{described_class::ROOT_TAG}/package/#{attribute}").text
|
result.at("//package/#{attribute}").text
|
||||||
).to eq(data[attribute])
|
).to eq(data[attribute])
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXml do
|
RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXmlService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
subject { described_class.new(data).execute }
|
subject { described_class.new(data).execute }
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Packages::Rpm::RepositoryMetadata::UpdateXmlService do
|
||||||
|
describe '#execute' do
|
||||||
|
subject { described_class.new(filename: filename, xml: xml, data: data).execute }
|
||||||
|
|
||||||
|
let(:xml) { nil }
|
||||||
|
let(:data) { nil }
|
||||||
|
|
||||||
|
shared_examples 'handling not implemented xml filename' do
|
||||||
|
let(:filename) { :not_implemented_yet }
|
||||||
|
let(:empty_xml) { '' }
|
||||||
|
|
||||||
|
it 'raise error' do
|
||||||
|
expect { subject }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'with primary xml file data' do
|
||||||
|
let(:filename) { :primary }
|
||||||
|
let(:empty_xml) do
|
||||||
|
<<~XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
|
||||||
|
XML
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'with other xml file data' do
|
||||||
|
let(:filename) { :other }
|
||||||
|
let(:empty_xml) do
|
||||||
|
<<~XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
|
||||||
|
XML
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when building empty xml' do
|
||||||
|
shared_examples 'generating empty xml' do
|
||||||
|
it 'generate expected xml' do
|
||||||
|
expect(subject).to eq(empty_xml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'handling not implemented xml filename'
|
||||||
|
|
||||||
|
context "for 'primary' xml file" do
|
||||||
|
include_context 'with primary xml file data'
|
||||||
|
|
||||||
|
it_behaves_like 'generating empty xml'
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for 'other' xml file" do
|
||||||
|
include_context 'with other xml file data'
|
||||||
|
|
||||||
|
it_behaves_like 'generating empty xml'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updating xml file' do
|
||||||
|
include_context 'with rpm package data'
|
||||||
|
|
||||||
|
let(:xml) { empty_xml }
|
||||||
|
let(:data) { xml_update_params }
|
||||||
|
let(:builder_class) { described_class::BUILDERS[filename] }
|
||||||
|
|
||||||
|
shared_examples 'updating rpm xml file' do
|
||||||
|
context 'when updating existing xml' do
|
||||||
|
shared_examples 'changing root tag attribute' do
|
||||||
|
it "increment previous 'packages' value by 1" do
|
||||||
|
previous_value = Nokogiri::XML(xml).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
|
||||||
|
new_value = Nokogiri::XML(subject).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
|
||||||
|
|
||||||
|
expect(previous_value + 1).to eq(new_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generate valid xml add expected xml node to existing xml' do
|
||||||
|
# Have one root attribute
|
||||||
|
result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
|
||||||
|
expect(result.children.count).to eq(1)
|
||||||
|
|
||||||
|
# Root node has 1 child with generated node
|
||||||
|
expect(result.xpath("//#{builder_class::ROOT_TAG}/package").count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when empty xml' do
|
||||||
|
it_behaves_like 'changing root tag attribute'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when xml has children' do
|
||||||
|
context "when node with given 'pkgid' does not exist yet" do
|
||||||
|
let(:uniq_node_data) do
|
||||||
|
xml_update_params.tap do |data|
|
||||||
|
data[:pkgid] = SecureRandom.uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:xml) { build_xml_from(uniq_node_data) }
|
||||||
|
|
||||||
|
it 'has children nodes' do
|
||||||
|
existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
|
||||||
|
expect(existing_xml.xpath('//package').count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'changing root tag attribute'
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when node with given 'pkgid' already exist" do
|
||||||
|
let(:existing_node_data) do
|
||||||
|
existing_data = data.dup
|
||||||
|
existing_data[:name] = FFaker::Lorem.word
|
||||||
|
existing_data
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:xml) { build_xml_from(existing_node_data) }
|
||||||
|
|
||||||
|
it 'has children nodes' do
|
||||||
|
existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
|
||||||
|
expect(existing_xml.xpath('//package').count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'replace existing node with new data' do
|
||||||
|
existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
|
||||||
|
result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
|
||||||
|
expect(result.xpath('//package').count).to eq(1)
|
||||||
|
expect(result.xpath('//package').first.to_xml).not_to eq(existing_xml.xpath('//package').first.to_xml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_xml_from(data)
|
||||||
|
described_class.new(filename: filename, xml: empty_xml, data: data).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'handling not implemented xml filename'
|
||||||
|
|
||||||
|
context "for 'primary' xml file" do
|
||||||
|
include_context 'with primary xml file data'
|
||||||
|
|
||||||
|
it_behaves_like 'updating rpm xml file'
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for 'other' xml file" do
|
||||||
|
include_context 'with other xml file data'
|
||||||
|
|
||||||
|
it_behaves_like 'updating rpm xml file'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_context 'when handling retried jobs' do |url|
|
||||||
|
let(:set_name) { 'retry' }
|
||||||
|
# Account for Sidekiq retry jitter
|
||||||
|
# https://github.com/mperham/sidekiq/blob/3575ccb44c688dd08bfbfd937696260b12c622fb/lib/sidekiq/job_retry.rb#L217
|
||||||
|
let(:schedule_jitter) { 10 }
|
||||||
|
|
||||||
|
# Try to mimic as closely as possible what Sidekiq will actually
|
||||||
|
# do to retry a job.
|
||||||
|
def retry_in(klass, time, args = 0)
|
||||||
|
message = Gitlab::Json.generate(
|
||||||
|
'class' => klass.name,
|
||||||
|
'args' => [args],
|
||||||
|
'retry' => true
|
||||||
|
)
|
||||||
|
|
||||||
|
allow(klass).to receive(:sidekiq_retry_in_block).and_return(proc { time })
|
||||||
|
|
||||||
|
begin
|
||||||
|
Sidekiq::JobRetry.new.local(klass, message, klass.queue) { raise 'boom' }
|
||||||
|
rescue Sidekiq::JobRetry::Skip
|
||||||
|
# Sidekiq scheduled the retry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,52 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.shared_examples 'handling rpm xml file' do
|
|
||||||
include_context 'with rpm package data'
|
|
||||||
|
|
||||||
let(:xml) { nil }
|
|
||||||
let(:data) { {} }
|
|
||||||
|
|
||||||
context 'when generate empty xml' do
|
|
||||||
it 'generate expected xml' do
|
|
||||||
expect(subject).to eq(empty_xml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when updating existing xml' do
|
|
||||||
let(:xml) { empty_xml }
|
|
||||||
let(:data) { xml_update_params }
|
|
||||||
|
|
||||||
shared_examples 'changing root tag attribute' do
|
|
||||||
it "increment previous 'packages' value by 1" do
|
|
||||||
previous_value = Nokogiri::XML(xml).at(described_class::ROOT_TAG).attributes["packages"].value.to_i
|
|
||||||
new_value = Nokogiri::XML(subject).at(described_class::ROOT_TAG).attributes["packages"].value.to_i
|
|
||||||
|
|
||||||
expect(previous_value + 1).to eq(new_value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'generate valid xml add expected xml node to existing xml' do
|
|
||||||
# Have one root attribute
|
|
||||||
result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
|
|
||||||
expect(result.children.count).to eq(1)
|
|
||||||
|
|
||||||
# Root node has 1 child with generated node
|
|
||||||
expect(result.xpath("//#{described_class::ROOT_TAG}/package").count).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when empty xml' do
|
|
||||||
it_behaves_like 'changing root tag attribute'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when xml has children' do
|
|
||||||
let(:xml) { described_class.new(xml: empty_xml, data: data).execute }
|
|
||||||
|
|
||||||
it 'has children nodes' do
|
|
||||||
result = Nokogiri::XML::Document.parse(xml).remove_namespaces!
|
|
||||||
expect(result.children.count).to be > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'changing root tag attribute'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue