diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 78769217d7c..b68508db993 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -2113,6 +2113,7 @@ Gitlab/NamespacedClass:
- 'ee/app/models/weight_note.rb'
- 'ee/app/policies/approval_merge_request_rule_policy.rb'
- 'ee/app/policies/approval_project_rule_policy.rb'
+ - 'ee/app/policies/approval_state_policy.rb'
- 'ee/app/policies/dast_scanner_profile_policy.rb'
- 'ee/app/policies/dast_site_profile_policy.rb'
- 'ee/app/policies/dast_site_validation_policy.rb'
diff --git a/app/assets/javascripts/content_editor/extensions/bullet_list.js b/app/assets/javascripts/content_editor/extensions/bullet_list.js
index 01ead571fe1..941a04daf98 100644
--- a/app/assets/javascripts/content_editor/extensions/bullet_list.js
+++ b/app/assets/javascripts/content_editor/extensions/bullet_list.js
@@ -1 +1,19 @@
-export { BulletList as default } from '@tiptap/extension-bullet-list';
+import { BulletList } from '@tiptap/extension-bullet-list';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+
+export default BulletList.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+
+ bullet: {
+ default: '*',
+ parseHTML(element) {
+ const bullet = getMarkdownSource(element)?.charAt(0);
+
+ return { bullet: '*+-'.includes(bullet) ? bullet : '*' };
+ },
+ },
+ };
+ },
+});
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 57e8de2914b..73d14f93e79 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -118,8 +118,6 @@ const defaultSerializerConfig = {
},
};
-const wrapHtmlPayload = (payload) => `
${payload}
`;
-
/**
* A markdown serializer converts arbitrary Markdown content
* into a ProseMirror document and viceversa. To convert Markdown
@@ -144,15 +142,15 @@ export default ({ render = () => null, serializerConfig = {} } = {}) => ({
deserialize: async ({ schema, content }) => {
const html = await render(content);
- if (!html) {
- return null;
- }
+ if (!html) return null;
const parser = new DOMParser();
- const {
- body: { firstElementChild },
- } = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
- const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
+ const { body } = parser.parseFromString(html, 'text/html');
+
+ // append original source as a comment that nodes can access
+ body.append(document.createComment(content));
+
+ const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
return state.toJSON();
},
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
new file mode 100644
index 00000000000..a1199589c9b
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -0,0 +1,40 @@
+const getFullSource = (element) => {
+ const commentNode = element.ownerDocument.body.lastChild;
+
+ if (commentNode.nodeName === '#comment') {
+ return commentNode.textContent.split('\n');
+ }
+
+ return [];
+};
+
+const getRangeFromSourcePos = (sourcePos) => {
+ const [start, end] = sourcePos.split('-');
+ const [startRow, startCol] = start.split(':');
+ const [endRow, endCol] = end.split(':');
+
+ return {
+ start: { row: Number(startRow) - 1, col: Number(startCol) - 1 },
+ end: { row: Number(endRow) - 1, col: Number(endCol) - 1 },
+ };
+};
+
+export const getMarkdownSource = (element) => {
+ if (!element.dataset.sourcepos) return undefined;
+
+ const source = getFullSource(element);
+ const range = getRangeFromSourcePos(element.dataset.sourcepos);
+ let elSource = '';
+
+ for (let i = range.start.row; i <= range.end.row; i += 1) {
+ if (i === range.start.row) {
+ elSource += source[i]?.substring(range.start.col);
+ } else if (i === range.end.row) {
+ elSource += `\n${source[i]?.substring(0, range.start.col)}`;
+ } else {
+ elSource += `\n${source[i]}` || '';
+ }
+ }
+
+ return elSource.trim();
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index c9ecac6829b..839e3769362 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -98,12 +98,7 @@ export default {
},
},
methods: {
- ...mapActions([
- 'fetchCycleAnalyticsData',
- 'fetchStageData',
- 'setSelectedStage',
- 'setDateRange',
- ]),
+ ...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
handleDateSelect(daysInPast) {
this.setDateRange(daysInPast);
},
diff --git a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
index 7eb40b12f51..b715633a9f2 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
@@ -1,4 +1,11 @@
fragment VersionListItem on DesignVersion {
id
sha
+ createdAt
+ author {
+ __typename
+ id
+ name
+ avatarUrl
+ }
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index 84aeb374351..111f5ac18a7 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -1,13 +1,15 @@
#import "../fragments/design.fragment.graphql"
+#import "../fragments/version.fragment.graphql"
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
designs {
...DesignItem
versions {
+ __typename
nodes {
- id
- sha
+ __typename
+ ...VersionListItem
}
}
}
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
index 05b220801f2..7470f3d259b 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -85,6 +85,13 @@ export const designUploadOptimisticResponse = (files) => {
__typename: 'DesignVersion',
id: -uniqueId(),
sha: -uniqueId(),
+ createdAt: '',
+ author: {
+ __typename: 'UserCore',
+ id: -uniqueId(),
+ name: '',
+ avatarUrl: '',
+ },
},
},
}));
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 275fecc5a32..53d3b853f03 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -43,7 +43,10 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile({ name, raw, content, mimeType = '' }) {
+export function isTextFile({ name, raw, binary, content, mimeType = '' }) {
+ // some file objects already have a `binary` property set on them. If true, return false
+ if (binary) return false;
+
const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 14d08caef34..0cd3519bcec 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -32,7 +32,7 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
...mapGetters([
'isLoading',
'isImportingAnyRepo',
@@ -43,7 +43,7 @@ export default {
]),
pagePaginationStateKey() {
- return `${this.filter}-${this.repositories.length}`;
+ return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
},
availableNamespaces() {
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 5cbc6e85bf3..92be028b8a9 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -53,7 +53,6 @@ const importAll = ({ state, dispatch }) => {
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
const nextPage = state.pageInfo.page + 1;
- commit(types.SET_PAGE, nextPage);
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -67,11 +66,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
}),
)
.then(({ data }) => {
+ commit(types.SET_PAGE, nextPage);
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
})
.catch((e) => {
- commit(types.SET_PAGE, nextPage - 1);
-
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index c5e1922597a..45f7a684161 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -9,7 +9,7 @@ const makeNewImportedProject = (importedProject) => ({
sanitizedName: importedProject.name,
providerLink: importedProject.providerLink,
},
- importedProject,
+ importedProject: { ...importedProject },
});
const makeNewIncompatibleProject = (project) => ({
@@ -63,15 +63,16 @@ export default {
factory: makeNewIncompatibleProject,
});
- state.repositories = [
- ...newImportedProjects,
- ...state.repositories,
- ...repositories.providerRepos.map((project) => ({
+ const existingProjects = [...newImportedProjects, ...state.repositories];
+ const existingProjectNames = new Set(existingProjects.map((p) => p.importSource.fullName));
+ const newProjects = repositories.providerRepos
+ .filter((project) => !existingProjectNames.has(project.fullName))
+ .map((project) => ({
importSource: project,
importedProject: null,
- })),
- ...newIncompatibleProjects,
- ];
+ }));
+
+ state.repositories = [...existingProjects, ...newProjects, ...newIncompatibleProjects];
if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
state.pageInfo.page -= 1;
diff --git a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
index 02e31d6fbb3..2290d6a078f 100644
--- a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
+++ b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
@@ -3,6 +3,10 @@ import { GlBanner } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { setCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import { EVENT_LABEL, DISMISS_EVENT, CLICK_EVENT } from '../constants';
+
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
export default {
name: 'TerraformNotification',
@@ -16,6 +20,7 @@ export default {
components: {
GlBanner,
},
+ mixins: [trackingMixin],
inject: ['terraformImagePath', 'bannerDismissedKey'],
data() {
return {
@@ -31,6 +36,10 @@ export default {
handleClose() {
setCookie(this.bannerDismissedKey, true);
this.isVisible = false;
+ this.track(DISMISS_EVENT);
+ },
+ buttonClick() {
+ this.track(CLICK_EVENT);
},
},
};
@@ -43,6 +52,7 @@ export default {
:button-link="docsUrl"
:svg-path="terraformImagePath"
variant="promotion"
+ @primary="buttonClick"
@close="handleClose"
>
{{ $options.i18n.description }}
diff --git a/app/assets/javascripts/projects/terraform_notification/constants.js b/app/assets/javascripts/projects/terraform_notification/constants.js
new file mode 100644
index 00000000000..029f40b2ab2
--- /dev/null
+++ b/app/assets/javascripts/projects/terraform_notification/constants.js
@@ -0,0 +1,3 @@
+export const EVENT_LABEL = 'terraform_banner';
+export const DISMISS_EVENT = 'dismiss_banner';
+export const CLICK_EVENT = 'click_button';
diff --git a/app/assets/stylesheets/page_bundles/cycle_analytics.scss b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
index 2248d95ae24..5d42ece32c9 100644
--- a/app/assets/stylesheets/page_bundles/cycle_analytics.scss
+++ b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
@@ -3,293 +3,4 @@
.cycle-analytics {
margin: 24px auto 0;
position: relative;
-
- .landing {
- margin-top: 0;
-
- .inner-content {
- white-space: normal;
-
- h4,
- p {
- margin: 7px 0 0;
- max-width: 480px;
- padding: 0 $gl-padding;
-
- @include media-breakpoint-down(sm) {
- margin: 0 auto;
- }
- }
- }
-
- .svg-container svg {
- width: 136px;
- height: 136px;
- }
- }
-
- .col-headers {
- ul {
- margin: 0;
- padding: 0;
- }
-
- li {
- line-height: 50px;
- }
- }
-
- .card {
- .content-block {
- padding: 24px 0;
- border-bottom: 0;
- position: relative;
-
- @include media-breakpoint-down(xs) {
- padding: 6px 0 24px;
- }
- }
-
- .column {
- text-align: center;
-
- @include media-breakpoint-down(xs) {
- padding: 15px 0;
- }
-
- .header {
- font-size: 30px;
- line-height: 38px;
- font-weight: $gl-font-weight-normal;
- margin: 0;
- }
-
- .text {
- color: var(--gray-500, $gray-500);
- margin: 0;
- }
-
- &:last-child {
- @include media-breakpoint-down(xs) {
- text-align: center;
- }
- }
- }
- }
-
- .stage-panel-body {
- display: flex;
- flex-wrap: wrap;
- }
-
- .stage-nav,
- .stage-entries {
- display: flex;
- vertical-align: top;
- font-size: $gl-font-size;
- }
-
- .stage-nav {
- width: 40%;
- margin-bottom: 0;
-
- ul {
- padding: 0;
- margin: 0;
- width: 100%;
- }
-
- li {
- list-style-type: none;
- }
-
- .stage-nav-item {
- line-height: 65px;
-
- &.active {
- background: var(--blue-50, $blue-50);
- border-color: var(--blue-300, $blue-300);
- box-shadow: inset 4px 0 0 0 var(--blue-500, $blue-500);
- }
-
- &:hover:not(.active) {
- background-color: var(--gray-10, $gray-10);
- box-shadow: inset 2px 0 0 0 var(--border-color, $border-color);
- cursor: pointer;
- }
-
- .stage-nav-item-cell.stage-name {
- width: 44.5%;
- }
-
- .stage-nav-item-cell.stage-median {
- min-width: 43%;
- }
-
- .stage-empty,
- .not-available {
- color: var(--gray-500, $gray-500);
- }
- }
- }
-
- .stage-panel-container {
- width: 100%;
- overflow: auto;
- }
-
- .stage-panel {
- min-width: 968px;
-
- .card-header {
- padding: 0;
- background-color: transparent;
- }
-
- .events-description {
- line-height: 65px;
- }
-
- .events-info {
- color: var(--gray-500, $gray-500);
- }
- }
-
- .stage-events {
- min-height: 467px;
- }
-
- .stage-event-list {
- margin: 0;
- padding: 0;
- }
-
- .stage-event-item {
- @include clearfix;
- list-style-type: none;
- padding-bottom: $gl-padding;
- margin-bottom: $gl-padding;
- border-bottom: 1px solid var(--gray-50, $gray-50);
-
- &:last-child {
- border-bottom: 0;
- margin-bottom: 0;
- }
-
- .item-details,
- .item-time {
- float: left;
- }
-
- .item-details {
- width: 75%;
- }
-
- .item-title {
- margin: 0 0 2px;
-
- &.issue-title,
- &.commit-title,
- &.merge-request-title {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- display: block;
-
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
- }
- }
-
- .item-time {
- width: 25%;
- text-align: right;
- }
-
- .total-time {
- font-size: $cycle-analytics-big-font;
- color: var(--gl-text-color, $gl-text-color);
-
- span {
- color: var(--gl-text-color, $gl-text-color);
- font-size: $gl-font-size;
- }
- }
-
- .issue-date,
- .build-date {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- .mr-link,
- .issue-link,
- .commit-author-link,
- .issue-author-link {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- // Custom CSS for components
- .item-conmmit-component {
- .commit-icon {
- svg {
- display: inline-block;
- width: 20px;
- height: 20px;
- vertical-align: bottom;
- }
- }
- }
-
- .merge-request-branch {
- a {
- max-width: 180px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
- }
-
- // Custom Styles for stage items
- .item-build-component {
- .item-title {
- .icon-build-status {
- float: left;
- margin-right: 5px;
- position: relative;
- top: 2px;
- }
-
- .item-build-name {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- .pipeline-id {
- color: var(--gl-text-color, $gl-text-color);
- padding: 0 3px 0 0;
- }
-
- .ref-name {
- color: var(--gray-900, $gray-900);
- display: inline-block;
- max-width: 180px;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- line-height: 1.3;
- vertical-align: top;
- }
-
- .commit-sha {
- color: var(--blue-600, $blue-600);
- line-height: 1.3;
- vertical-align: top;
- font-weight: $gl-font-weight-normal;
- }
- }
- }
}
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 57bd39bbe06..d32755dbd94 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -62,14 +62,10 @@ class Import::BitbucketController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- already_added_projects_names = already_added_projects.map(&:import_source)
-
- bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) || !repo.valid? }
+ bitbucket_repos.filter { |repo| repo.valid? }
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 1846b1e0cec..31e9694ca1d 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -62,16 +62,10 @@ class Import::BitbucketServerController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- # Use the import URL to filter beyond what BaseService#find_already_added_projects
- already_added_projects = filter_added_projects('bitbucket_server', bitbucket_repos.map(&:browse_url))
- already_added_projects_names = already_added_projects.map(&:import_source)
-
- bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.browse_url) || !repo.valid? }
+ bitbucket_repos.filter { |repo| repo.valid? }
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
@@ -90,12 +84,6 @@ class Import::BitbucketServerController < Import::BaseController
private
- # rubocop: disable CodeReuse/ActiveRecord
- def filter_added_projects(import_type, import_sources)
- current_user.created_projects.where(import_type: import_type, import_source: import_sources).with_import_state
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def client
@client ||= BitbucketServer::Client.new(credentials)
end
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 9f91f3a1e1c..377292d47d8 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -74,16 +74,10 @@ class Import::FogbugzController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- repos = client.repos
-
- already_added_projects_names = already_added_projects.map(&:import_source)
-
- repos.reject { |repo| already_added_projects_names.include? repo.name }
+ client.repos
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 22bcd14d664..d7aebd25432 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -64,9 +64,7 @@ class Import::GithubController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- already_added_projects_names = already_added_projects.pluck(:import_source)
-
- client_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) }
+ client_repos.to_a
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index cc68eb02741..662b02010ba 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -39,16 +39,10 @@ class Import::GitlabController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
-
- already_added_projects_names = already_added_projects.map(&:import_source)
-
- repos.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
+ client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index 8497e15c07c..956d0c9a2ae 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -41,7 +41,7 @@ class Import::ManifestController < Import::BaseController
end
def create
- repository = repositories.find do |project|
+ repository = importable_repos.find do |project|
project[:id] == params[:repo_id].to_i
end
@@ -56,14 +56,10 @@ class Import::ManifestController < Import::BaseController
protected
- # rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
- already_added_projects_names = already_added_projects.pluck(:import_url)
-
- repositories.reject { |repo| already_added_projects_names.include?(repo[:url]) }
+ @importable_repos ||= manifest_import_metadata.repositories
end
- # rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
@@ -88,7 +84,7 @@ class Import::ManifestController < Import::BaseController
private
def ensure_import_vars
- unless group && repositories.present?
+ unless group && importable_repos.present?
redirect_to(new_import_manifest_path)
end
end
@@ -103,10 +99,6 @@ class Import::ManifestController < Import::BaseController
@manifest_import_status ||= Gitlab::ManifestImport::Metadata.new(current_user, fallback: session)
end
- def repositories
- @repositories ||= manifest_import_metadata.repositories
- end
-
def find_jobs
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 0e4aeaae20d..0fdb8b07260 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,14 +1,6 @@
# frozen_string_literal: true
module GroupsHelper
- def group_sidebar_links
- @group_sidebar_links ||= get_group_sidebar_links
- end
-
- def group_sidebar_link?(link)
- group_sidebar_links.include?(link)
- end
-
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
@@ -33,20 +25,6 @@ module GroupsHelper
Ability.allowed?(current_user, :admin_group_member, group)
end
- def group_issues_count(state:)
- IssuesFinder
- .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
- .execute
- .count
- end
-
- def group_merge_requests_count(state:)
- MergeRequestsFinder
- .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
- .execute
- .count
- end
-
def group_dependency_proxy_image_prefix(group)
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
@@ -181,36 +159,6 @@ module GroupsHelper
group.member_count > 1 || group.members_with_parents.count > 1
end
- def get_group_sidebar_links
- links = [:overview, :group_members]
-
- resources = [:activity, :issues, :boards, :labels, :milestones,
- :merge_requests]
- links += resources.select do |resource|
- can?(current_user, "read_group_#{resource}".to_sym, @group)
- end
-
- # TODO Proper policies, such as `read_group_runners, should be implemented per
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
- if can?(current_user, :admin_group, @group) && Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
- links << :runners
- end
-
- if can?(current_user, :read_cluster, @group)
- links << :kubernetes
- end
-
- if can?(current_user, :admin_group, @group)
- links << :settings
- end
-
- if can?(current_user, :read_wiki, @group)
- links << :wiki
- end
-
- links
- end
-
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
icon = group_icon(group, class: "avatar-tile", width: 15, height: 15) if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
index 9185547d7cd..c12309d1852 100644
--- a/app/models/onboarding_progress.rb
+++ b/app/models/onboarding_progress.rb
@@ -45,7 +45,7 @@ class OnboardingProgress < ApplicationRecord
def onboard(namespace)
return unless root_namespace?(namespace)
- safe_find_or_create_by(namespace: namespace)
+ create(namespace: namespace)
end
def onboarding?(namespace)
diff --git a/app/views/devise/shared/_footer.html.haml b/app/views/devise/shared/_footer.html.haml
index ca1adb48543..5803107a8f7 100644
--- a/app/views/devise/shared/_footer.html.haml
+++ b/app/views/devise/shared/_footer.html.haml
@@ -4,5 +4,5 @@
- unless public_visibility_restricted?
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
- = link_to _("About GitLab"), "https://about.gitlab.com/"
+ = link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= footer_message
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 980730bc3be..c2b50bc0e52 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,3 +1 @@
--# We're migration the group sidebar to a logical model based structure. If you need to update
--# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user))
diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml
deleted file mode 100644
index 4c0ed6a888d..00000000000
--- a/app/views/layouts/nav/sidebar/_group_menus.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'shared/sidebar_toggle_button'
diff --git a/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml b/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml
new file mode 100644
index 00000000000..ebffae2a446
--- /dev/null
+++ b/config/feature_flags/development/add_namespace_and_project_to_snowplow_tracking.yml
@@ -0,0 +1,8 @@
+---
+name: add_namespace_and_project_to_snowplow_tracking
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68277
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338670
+milestone: '14.3'
+type: development
+group: group::product intelligence
+default_enabled: false
diff --git a/config/feature_flags/development/roadmap_daterange_filter.yml b/config/feature_flags/development/roadmap_daterange_filter.yml
new file mode 100644
index 00000000000..276242427c2
--- /dev/null
+++ b/config/feature_flags/development/roadmap_daterange_filter.yml
@@ -0,0 +1,8 @@
+---
+name: roadmap_daterange_filter
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55639
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323917
+milestone: '14.3'
+type: development
+group: group::product planning
+default_enabled: false
diff --git a/config/initializers_before_autoloader/001_fast_gettext.rb b/config/initializers_before_autoloader/001_fast_gettext.rb
index ede38450582..76a1dafd2d8 100644
--- a/config/initializers_before_autoloader/001_fast_gettext.rb
+++ b/config/initializers_before_autoloader/001_fast_gettext.rb
@@ -1,8 +1,31 @@
# frozen_string_literal: true
-FastGettext.add_text_domain 'gitlab',
- path: File.join(Rails.root, 'locale'),
- type: :po,
- ignore_fuzzy: true
+translation_repositories = [
+ FastGettext::TranslationRepository.build(
+ 'gitlab',
+ path: File.join(Rails.root, 'locale'),
+ type: :po,
+ ignore_fuzzy: true
+ )
+]
+
+Gitlab.jh do
+ translation_repositories.unshift(
+ FastGettext::TranslationRepository.build(
+ 'gitlab',
+ path: File.join(Rails.root, 'jh', 'locale'),
+ type: :po,
+ ignore_fuzzy: true
+ )
+ )
+end
+
+FastGettext.add_text_domain(
+ 'gitlab',
+ type: :chain,
+ chain: translation_repositories,
+ ignore_fuzzy: true
+)
+
FastGettext.default_text_domain = 'gitlab'
FastGettext.default_locale = :en
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 1ea2ec4c904..6c18f416e89 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -577,9 +577,9 @@ panel_groups:
See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-a-project-to-the-dashboard) for the maximum number of displayed projects.
-## Environment data on Deploy Boards
+## Environment data on deploy boards
-[Deploy Boards](../user/project/deploy_boards.md) load information from Kubernetes about
+[Deploy boards](../user/project/deploy_boards.md) load information from Kubernetes about
Pods and Deployments. However, data over 10 MB for a certain environment read from
Kubernetes won't be shown.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index d00739420bb..6600901e187 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7568,9 +7568,19 @@ Describes a rule for who can approve merge requests.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `approvalsRequired` | [`Int`](#int) | Number of required approvals. |
+| `approved` | [`Boolean`](#boolean) | Indicates if the rule is satisfied. |
+| `approvedBy` | [`UserCoreConnection`](#usercoreconnection) | List of users defined in the rule that approved the merge request. (see [Connections](#connections)) |
+| `containsHiddenGroups` | [`Boolean`](#boolean) | Indicates if the rule contains approvers from a hidden group. |
+| `eligibleApprovers` | [`UserCoreConnection`](#usercoreconnection) | List of all users eligible to approve the merge request (defined explicitly and from associated groups). (see [Connections](#connections)) |
+| `groups` | [`GroupConnection`](#groupconnection) | List of groups added as approvers for the rule. (see [Connections](#connections)) |
| `id` | [`GlobalID!`](#globalid) | ID of the rule. |
| `name` | [`String`](#string) | Name of the rule. |
+| `overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. |
+| `section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. |
+| `sourceRule` | [`ApprovalRule`](#approvalrule) | Source rule used to create the rule. |
| `type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. |
+| `users` | [`UserCoreConnection`](#usercoreconnection) | List of users added as approvers for the rule. (see [Connections](#connections)) |
### `AwardEmoji`
@@ -10600,6 +10610,7 @@ Maven metadata.
| Name | Type | Description |
| ---- | ---- | ----------- |
| `allowCollaboration` | [`Boolean`](#boolean) | Indicates if members of the target project can push to the fork. |
+| `approvalState` | [`MergeRequestApprovalState!`](#mergerequestapprovalstate) | Information relating to rules that must be satisfied to merge this merge request. |
| `approvalsLeft` | [`Int`](#int) | Number of approvals left. |
| `approvalsRequired` | [`Int`](#int) | Number of approvals required. |
| `approved` | [`Boolean!`](#boolean) | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
@@ -10746,6 +10757,17 @@ Returns [`String!`](#string).
| ---- | ---- | ----------- |
| `full` | [`Boolean`](#boolean) | Boolean option specifying whether the reference should be returned in full. |
+### `MergeRequestApprovalState`
+
+Information relating to rules that must be satisfied to merge this merge request.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `approvalRulesOverwritten` | [`Boolean`](#boolean) | Indicates if the merge request approval rules are overwritten for the merge request. |
+| `rules` | [`[ApprovalRule!]`](#approvalrule) | List of approval rules associated with the merge request. |
+
### `MergeRequestAssignee`
A user assigned to a merge request.
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index 1b4d8890c6e..4a77e6d84b4 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -732,7 +732,7 @@ the `review/feature-1` spec takes precedence over `review/*` and `*` specs.
- [Use GitLab CI to deploy to multiple environments (blog post)](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/)
- [Review Apps](../review_apps/index.md): Use dynamic environments to deploy your code for every branch.
-- [Deploy Boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
+- [Deploy boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
- [Protected environments](protected_environments.md): Determine who can deploy code to your environments.
- [Environments Dashboard](../environments/environments_dashboard.md): View a summary of each
environment's operational health. **(PREMIUM)**
diff --git a/doc/ci/index.md b/doc/ci/index.md
index 9175c20f580..30c36d0a061 100644
--- a/doc/ci/index.md
+++ b/doc/ci/index.md
@@ -107,7 +107,7 @@ Its feature set is listed on the table below according to DevOps stages.
| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
| [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
-| [Deploy Boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
+| [Deploy boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [Feature Flags](../operations/feature_flags.md) **(PREMIUM)** | Deploy your features behind Feature Flags. |
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 240b78a741f..3ba9af1bf98 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -90,6 +90,10 @@ Do not use. Instead, use **select** with buttons, links, menu items, and lists.
Do not use when talking about the product or its features. The documentation describes the product as it is today. ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml))
+## deploy board
+
+Lowercase.
+
## Developer
When writing about the Developer role:
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 2cf5a5befd7..5d3a04c7096 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -244,7 +244,7 @@ you to common environment tasks:
- **Stop environment** (**{stop}**) - For more information, see
[Stopping an environment](../../ci/environments/index.md#stop-an-environment)
-GitLab displays the [Deploy Board](../../user/project/deploy_boards.md) below the
+GitLab displays the [deploy board](../../user/project/deploy_boards.md) below the
environment's information, with squares representing pods in your
Kubernetes cluster, color-coded to show their status. Hovering over a square on
the deploy board displays the state of the deployment, and selecting the square
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index cb721115e76..cad55f0cf0b 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -36,7 +36,7 @@ In order to:
[deploy to a Kubernetes cluster](../project/clusters/index.md#deploying-to-a-kubernetes-cluster)
successfully.
- Show pod usage correctly, you must
- [enable Deploy Boards](../project/deploy_boards.md#enabling-deploy-boards).
+ [enable deploy boards](../project/deploy_boards.md#enabling-deploy-boards).
After you have successful deployments to your group-level or instance-level cluster:
diff --git a/doc/user/index.md b/doc/user/index.md
index 104da206e5f..d6eaad469c1 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -66,7 +66,7 @@ With GitLab Enterprise Edition, you can also:
- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](../ci/pipelines/multi_project_pipelines.md).
- [Lock files](project/file_lock.md) to prevent conflicts.
-- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](project/deploy_boards.md).
+- View the current health and status of each CI environment running on Kubernetes with [deploy boards](project/deploy_boards.md).
- Leverage continuous delivery method with [Canary Deployments](project/canary_deployments.md).
- Scan your code for vulnerabilities and [display them in merge requests](application_security/sast/index.md).
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index cac283f6f18..b4723294438 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -26,7 +26,7 @@ percentage of users are affected and the change can either be fixed or quickly
reverted.
Leveraging [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments), visualize your canary
-deployments right inside the [Deploy Board](deploy_boards.md), without the need to leave GitLab.
+deployments right inside the [deploy board](deploy_boards.md), without the need to leave GitLab.
## Use cases
@@ -47,9 +47,9 @@ this document.
## Enabling Canary Deployments
-Canary deployments require that you properly configure Deploy Boards:
+Canary deployments require that you properly configure deploy boards:
-1. Follow the steps to [enable Deploy Boards](deploy_boards.md#enabling-deploy-boards).
+1. Follow the steps to [enable deploy boards](deploy_boards.md#enabling-deploy-boards).
1. To track canary deployments you need to label your Kubernetes deployments and
pods with `track: canary`. To get started quickly, you can use the [Auto Deploy](../../topics/autodevops/stages.md#auto-deploy)
template for canary deployments that GitLab provides.
@@ -61,13 +61,13 @@ This allows GitLab to discover whether a deployment is stable or canary (tempora
Once all of the above are set up and the pipeline has run at least once,
navigate to the environments page under **Pipelines > Environments**.
-As the pipeline executes, Deploy Boards clearly mark canary pods, enabling
+As the pipeline executes, deploy boards clearly mark canary pods, enabling
quick and easy insight into the status of each environment and deployment.
-Canary deployments are marked with a yellow dot in the Deploy Board so that you
+Canary deployments are marked with a yellow dot in the deploy board so that you
can easily notice them.
-
+
### Advanced traffic control with Canary Ingress
@@ -104,17 +104,17 @@ Here's an example setup flow from scratch:
#### How to check the current traffic weight on a Canary Ingress
-1. Visit the [Deploy Board](../../user/project/deploy_boards.md).
+1. Visit the [deploy board](../../user/project/deploy_boards.md).
1. View the current weights on the right.

#### How to change the traffic weight on a Canary Ingress
-You can change the traffic weight within your environment's Deploy Board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
+You can change the traffic weight within your environment's deploy board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
or by sending requests to the [GraphQL API](../../api/graphql/getting_started.md#command-line).
-To use your [Deploy Board](../../user/project/deploy_boards.md):
+To use your [deploy board](../../user/project/deploy_boards.md):
1. Navigate to **Deployments > Environments** for your project.
1. Set the new weight with the dropdown on the right side.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index a0efea267f0..c534c30c75e 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -30,7 +30,7 @@ features such as:
- Use [role-based or attribute-based access controls](cluster_access.md).
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
- Connect GitLab to in-cluster applications using [cluster integrations](../../clusters/integrations.md).
-- Use [Deploy Boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
+- Use [deploy boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
- Use [Canary deployments](../canary_deployments.md) to update only a portion of your fleet with the latest version of your application.
- View your [Kubernetes podlogs](kubernetes_pod_logs.md) directly in GitLab.
- Connect to your cluster through GitLab [web terminals](deploy_to_cluster.md#web-terminals-for-kubernetes-clusters).
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 7a9c7eb423d..eb0e8d0e91c 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -46,15 +46,15 @@ a [metrics dashboard](../../../operations/metrics/index.md) and select **View lo
[permissions](../../permissions.md#project-members-permissions) in the project.
1. To navigate to the **Log Explorer** from the sidebar menu, go to **Monitor > Logs**
([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 12.5.).
-1. To navigate to the **Log Explorer** from a specific pod on a [Deploy Board](../deploy_boards.md):
+1. To navigate to the **Log Explorer** from a specific pod on a [deploy board](../deploy_boards.md):
1. Go to **Deployments > Environments** and find the environment
which contains the desired pod, like `production`.
1. On the **Environments** page, you should see the status of the environment's
- pods with [Deploy Boards](../deploy_boards.md).
+ pods with [deploy boards](../deploy_boards.md).
1. When mousing over the list of pods, GitLab displays a tooltip with the exact pod name
and status.
- 
+ 
1. Click on the desired pod to display the **Log Explorer**.
### Logs view
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index 64a5515136b..05f026cca18 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto, reference
---
-# Deploy Boards **(FREE)**
+# Deploy boards **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1589) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212320) to GitLab Free in 13.8.
@@ -16,7 +16,7 @@ type: howto, reference
> This is [fixed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60525) in
> GitLab 13.12.
-GitLab Deploy Boards offer a consolidated view of the current health and
+GitLab deploy boards offer a consolidated view of the current health and
status of each CI [environment](../../ci/environments/index.md) running on [Kubernetes](https://kubernetes.io), displaying the status
of the pods in the deployment. Developers and other teammates can view the
progress and status of a rollout, pod by pod, in the workflow they already use
@@ -28,23 +28,23 @@ environments by using [Auto DevOps](../../topics/autodevops/index.md).
## Overview
-With Deploy Boards you can gain more insight into deploys with benefits such as:
+With deploy boards you can gain more insight into deploys with benefits such as:
- Following a deploy from the start, not just when it's done
- Watching the rollout of a build across multiple servers
- Finer state detail (Succeeded, Running, Failed, Pending, Unknown)
- See [Canary Deployments](canary_deployments.md)
-Here's an example of a Deploy Board of the production environment.
+Here's an example of a deploy board of the production environment.
-
+
The squares represent pods in your Kubernetes cluster that are associated with
the given environment. Hovering above each square you can see the state of a
deploy rolling out. The percentage is the percent of the pods that are updated
to the latest release.
-Since Deploy Boards are tightly coupled with Kubernetes, there is some required
+Since deploy boards are tightly coupled with Kubernetes, there is some required
knowledge. In particular, you should be familiar with:
- [Kubernetes pods](https://kubernetes.io/docs/concepts/workloads/pods/)
@@ -54,7 +54,7 @@ knowledge. In particular, you should be familiar with:
## Use cases
-Since the Deploy Board is a visual representation of the Kubernetes pods for a
+Since the deploy board is a visual representation of the Kubernetes pods for a
specific environment, there are a lot of use cases. To name a few:
- You want to promote what's running in staging, to production. You go to the
@@ -73,9 +73,9 @@ specific environment, there are a lot of use cases. To name a few:
list, find the [Review App](../../ci/review_apps/index.md) you're interested in, and click the
manual action to deploy it to staging.
-## Enabling Deploy Boards
+## Enabling deploy boards
-To display the Deploy Boards for a specific [environment](../../ci/environments/index.md) you should:
+To display the deploy boards for a specific [environment](../../ci/environments/index.md) you should:
1. Have [defined an environment](../../ci/environments/index.md) with a deploy stage.
@@ -83,7 +83,7 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
NOTE:
If you're using OpenShift, ensure that you're using the `Deployment` resource
- instead of `DeploymentConfiguration`. Otherwise, the Deploy Boards don't render
+ instead of `DeploymentConfiguration`. Otherwise, the deploy boards don't render
correctly. For more information, read the
[OpenShift docs](https://docs.openshift.com/container-platform/3.7/dev_guide/deployments/kubernetes_deployments.html#kubernetes-deployments-vs-deployment-configurations)
and [GitLab issue #4584](https://gitlab.com/gitlab-org/gitlab/-/issues/4584).
@@ -114,17 +114,17 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
If you use GCP to manage clusters, you can see the deployment details in GCP itself by navigating to **Workloads > deployment name > Details**:
- 
+ 
Once all of the above are set up and the pipeline has run at least once,
navigate to the environments page under **Deployments > Environments**.
-Deploy Boards are visible by default. You can explicitly click
+Deploy boards are visible by default. You can explicitly click
the triangle next to their respective environment name in order to hide them.
### Example manifest file
-The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **Deploy Boards**:
+The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **deploy boards**:
```yaml
apiVersion: apps/v1
diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md
index 2b07131df6e..ed6c07f2c6d 100644
--- a/doc/user/project/issues/crosslinking_issues.md
+++ b/doc/user/project/issues/crosslinking_issues.md
@@ -19,14 +19,20 @@ issue itself and the first commit related to that issue.
If the issue and the code you're committing are both in the same project,
add `#xxx` to the commit message, where `xxx` is the issue number.
-If they are not in the same project, you can add the full URL to the issue
-(`https://gitlab.com///issues/`).
```shell
git commit -m "this is my commit message. Ref #xxx"
```
-or
+If they are in different projects, but in the same group,
+add `projectname#xxx` to the commit message.
+
+```shell
+git commit -m "this is my commit message. Ref projectname#xxx"
+```
+
+If they are not in the same group, you can add the full URL to the issue
+(`https://gitlab.com///issues/`).
```shell
git commit -m "this is my commit message. Related to https://gitlab.com///issues/"
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index d3bc3a38f1f..6feb693221b 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -5,9 +5,6 @@ module Gitlab
module Pipeline
module Chain
class Build < Chain::Base
- include Gitlab::Allowable
- include Chain::Helpers
-
def perform!
@pipeline.assign_attributes(
source: @command.source,
@@ -23,35 +20,12 @@ module Gitlab
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
external_pull_request: @command.external_pull_request,
- locked: @command.project.default_pipeline_lock,
- variables_attributes: variables_attributes
- )
+ locked: @command.project.default_pipeline_lock)
end
def break?
@pipeline.errors.any?
end
-
- private
-
- def variables_attributes
- variables = Array(@command.variables_attributes)
-
- # We allow parent pipelines to pass variables to child pipelines since
- # these variables are coming from internal configurations. We will check
- # permissions to :set_pipeline_variables when those are injected upstream,
- # to the parent pipeline.
- # In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
- # the variables are provided from the outside and those should be guarded.
- return variables if @command.creates_child_pipeline?
-
- if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
- error("Insufficient permissions to set pipeline variables")
- variables = []
- end
-
- variables
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/build/associations.rb b/lib/gitlab/ci/pipeline/chain/build/associations.rb
index eb49c56bcd7..b5d63691849 100644
--- a/lib/gitlab/ci/pipeline/chain/build/associations.rb
+++ b/lib/gitlab/ci/pipeline/chain/build/associations.rb
@@ -6,7 +6,25 @@ module Gitlab
module Chain
class Build
class Associations < Chain::Base
+ include Gitlab::Allowable
+ include Chain::Helpers
+
def perform!
+ assign_pipeline_variables
+ assign_source_pipeline
+ end
+
+ def break?
+ @pipeline.errors.any?
+ end
+
+ private
+
+ def assign_pipeline_variables
+ @pipeline.variables_attributes = variables_attributes
+ end
+
+ def assign_source_pipeline
return unless @command.bridge
@pipeline.build_source_pipeline(
@@ -17,8 +35,45 @@ module Gitlab
)
end
- def break?
- false
+ def variables_attributes
+ variables = Array(@command.variables_attributes)
+ variables = apply_permissions(variables)
+ validate_uniqueness(variables)
+ end
+
+ def apply_permissions(variables)
+ # We allow parent pipelines to pass variables to child pipelines since
+ # these variables are coming from internal configurations. We will check
+ # permissions to :set_pipeline_variables when those are injected upstream,
+ # to the parent pipeline.
+ # In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
+ # the variables are provided from the outside and those should be guarded.
+ return variables if @command.creates_child_pipeline?
+
+ if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
+ error("Insufficient permissions to set pipeline variables")
+ variables = []
+ end
+
+ variables
+ end
+
+ def validate_uniqueness(variables)
+ duplicated_keys = variables
+ .map { |var| var[:key] }
+ .tally
+ .filter_map { |key, count| key if count > 1 }
+
+ if duplicated_keys.empty?
+ variables
+ else
+ error(duplicate_variables_message(duplicated_keys), config_error: true)
+ []
+ end
+ end
+
+ def duplicate_variables_message(keys)
+ "Duplicate variable #{'name'.pluralize(keys.size)}: #{keys.join(', ')}"
end
end
end
diff --git a/lib/gitlab/tracking/standard_context.rb b/lib/gitlab/tracking/standard_context.rb
index 7902f96dfa6..fe5669be014 100644
--- a/lib/gitlab/tracking/standard_context.rb
+++ b/lib/gitlab/tracking/standard_context.rb
@@ -8,7 +8,8 @@ module Gitlab
def initialize(namespace: nil, project: nil, user: nil, **extra)
@namespace = namespace
- @plan = @namespace&.actual_plan_name
+ @plan = namespace&.actual_plan_name
+ @project = project
@extra = extra
end
@@ -34,14 +35,29 @@ module Gitlab
private
+ attr_accessor :namespace, :project, :extra, :plan
+
def to_h
{
environment: environment,
source: source,
- plan: @plan,
- extra: @extra
+ plan: plan,
+ extra: extra
+ }.merge(project_and_namespace)
+ end
+
+ def project_and_namespace
+ return {} unless ::Feature.enabled?(:add_namespace_and_project_to_snowplow_tracking, default_enabled: :yaml)
+
+ {
+ namespace_id: namespace&.id,
+ project_id: project_id
}
end
+
+ def project_id
+ project.is_a?(Integer) ? project : project&.id
+ end
end
end
end
diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb
index 73b943c5655..6efe89d496a 100644
--- a/lib/sidebars/groups/panel.rb
+++ b/lib/sidebars/groups/panel.rb
@@ -16,11 +16,6 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::SettingsMenu.new(context))
end
- override :render_raw_menus_partial
- def render_raw_menus_partial
- 'layouts/nav/sidebar/group_menus'
- end
-
override :aria_label
def aria_label
context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8e6cfa285f8..94ce205e36f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -15905,6 +15905,12 @@ msgstr ""
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
msgstr ""
+msgid "GroupRoadmap|This quarter"
+msgstr ""
+
+msgid "GroupRoadmap|This year"
+msgstr ""
+
msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them."
msgstr ""
@@ -15917,6 +15923,9 @@ msgstr ""
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
msgstr ""
+msgid "GroupRoadmap|Within 3 years"
+msgstr ""
+
msgid "GroupSAML|%{strongOpen}Warning%{strongClose} - Enabling %{linkStart}SSO enforcement%{linkEnd} can reduce security risks."
msgstr ""
diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb
index d5a498e80d9..0111ad9501f 100644
--- a/spec/controllers/import/manifest_controller_spec.rb
+++ b/spec/controllers/import/manifest_controller_spec.rb
@@ -75,16 +75,6 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo1[:id])
expect(json_response.dig("provider_repos", 1, "id")).to eq(repo2[:id])
end
-
- it "does not show already added project" do
- project = create(:project, import_type: 'manifest', namespace: user.namespace, import_status: :finished, import_url: repo1[:url])
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos").length).to eq(1)
- expect(json_response.dig("provider_repos", 0, "id")).not_to eq(repo1[:id])
- end
end
context 'when the data is stored via Gitlab::ManifestImport::Metadata' do
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index 1334b1ddaad..97a33b28cdd 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -76,9 +76,7 @@ describe('content_editor/extensions/attachment', () => {
const base64EncodedFile = 'data:image/png;base64,Zm9v';
beforeEach(() => {
- renderMarkdown.mockResolvedValue(
- loadMarkdownApiResult('project_wiki_attachment_image').body,
- );
+ renderMarkdown.mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image'));
});
describe('when uploading succeeds', () => {
@@ -153,7 +151,7 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
- const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link').body;
+ const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link');
beforeEach(() => {
renderMarkdown.mockResolvedValue(markdownApiResult);
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 188e6580dc6..828fdb224fc 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -11,10 +11,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
- const { html } = loadMarkdownApiResult('code_block');
-
tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
- codeBlockHtmlFixture = html;
+ codeBlockHtmlFixture = loadMarkdownApiResult('code_block');
parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
tiptapEditor.commands.setContent(codeBlockHtmlFixture);
diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js
index 12eed00f3c6..b3aabfeb145 100644
--- a/spec/frontend/content_editor/markdown_processing_examples.js
+++ b/spec/frontend/content_editor/markdown_processing_examples.js
@@ -6,7 +6,8 @@ import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
- return getJSONFixture(fixturePathPrefix);
+ const fixture = getJSONFixture(fixturePathPrefix);
+ return fixture.body || fixture.html;
};
export const loadMarkdownApiExamples = () => {
@@ -16,3 +17,9 @@ export const loadMarkdownApiExamples = () => {
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
};
+
+export const loadMarkdownApiExample = (testName) => {
+ return loadMarkdownApiExamples().find(([name, context]) => {
+ return (context ? `${context}_${name}` : name) === testName;
+ })[2];
+};
diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js
index da3f6e64db8..71565768558 100644
--- a/spec/frontend/content_editor/markdown_processing_spec.js
+++ b/spec/frontend/content_editor/markdown_processing_spec.js
@@ -9,8 +9,9 @@ describe('markdown processing', () => {
'correctly handles %s (context: %s)',
async (name, context, markdown) => {
const testName = context ? `${context}_${name}` : name;
- const { html, body } = loadMarkdownApiResult(testName);
- const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
+ const contentEditor = createContentEditor({
+ renderMarkdown: () => loadMarkdownApiResult(testName),
+ });
await contentEditor.setSerializedContent(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown);
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
new file mode 100644
index 00000000000..0ef822942ea
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -0,0 +1,71 @@
+import { Extension } from '@tiptap/core';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import ListItem from '~/content_editor/extensions/list_item';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
+import { loadMarkdownApiResult, loadMarkdownApiExample } from '../markdown_processing_examples';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+const SourcemapExtension = Extension.create({
+ // lets add `source` attribute to every element using `getMarkdownSource`
+ addGlobalAttributes() {
+ return [
+ {
+ types: [Paragraph.name, BulletList.name, ListItem.name],
+ attributes: {
+ source: {
+ parseHTML: (element) => {
+ const source = getMarkdownSource(element);
+ if (source) return { source };
+ return {};
+ },
+ },
+ },
+ },
+ ];
+ },
+});
+
+const tiptapEditor = createTestEditor({
+ extensions: [BulletList, ListItem, SourcemapExtension],
+});
+
+const {
+ builders: { doc, bulletList, listItem, paragraph },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ bulletList: { nodeType: BulletList.name },
+ listItem: { nodeType: ListItem.name },
+ },
+});
+
+describe('content_editor/services/markdown_sourcemap', () => {
+ it('gets markdown source for a rendered HTML element', async () => {
+ const deserialized = await markdownSerializer({
+ render: () => loadMarkdownApiResult('bullet_list_style_3'),
+ serializerConfig: {},
+ }).deserialize({
+ schema: tiptapEditor.schema,
+ content: loadMarkdownApiExample('bullet_list_style_3'),
+ });
+
+ const expected = doc(
+ bulletList(
+ { bullet: '+', source: '+ list item 1\n+ list item 2' },
+ listItem({ source: '+ list item 1' }, paragraph('list item 1')),
+ listItem(
+ { source: '+ list item 2' },
+ paragraph('list item 2'),
+ bulletList(
+ { bullet: '-', source: '- embedded list item 3' },
+ listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
+ ),
+ ),
+ ),
+ );
+
+ expect(deserialized).toEqual(expected.toJSON());
+ });
+});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 95cb1ac943c..d35c5398b20 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -338,6 +338,13 @@ describe('Design management index page', () => {
__typename: 'DesignVersion',
id: expect.anything(),
sha: expect.anything(),
+ createdAt: '',
+ author: {
+ __typename: 'UserCore',
+ id: expect.anything(),
+ name: '',
+ avatarUrl: '',
+ },
},
},
},
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
index 5b7f99e9d96..dc6056badb9 100644
--- a/spec/frontend/design_management/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -101,7 +101,13 @@ describe('optimistic responses', () => {
discussions: { __typename: 'DesignDiscussion', nodes: [] },
versions: {
__typename: 'DesignVersionConnection',
- nodes: { __typename: 'DesignVersion', id: -1, sha: -1 },
+ nodes: {
+ __typename: 'DesignVersion',
+ id: expect.anything(),
+ sha: expect.anything(),
+ createdAt: '',
+ author: { __typename: 'UserCore', avatarUrl: '', name: '', id: expect.anything() },
+ },
},
},
],
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 09a57e04631..6924423eecc 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -66,11 +66,21 @@
- name: thematic_break
markdown: |-
---
-- name: bullet_list
+- name: bullet_list_style_1
markdown: |-
* list item 1
* list item 2
* embedded list item 3
+- name: bullet_list_style_2
+ markdown: |-
+ - list item 1
+ - list item 2
+ * embedded list item 3
+- name: bullet_list_style_3
+ markdown: |-
+ + list item 1
+ + list item 2
+ - embedded list item 3
- name: ordered_list
markdown: |-
1. list item 1
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 00733615f81..2f58cd8f22a 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -86,6 +86,11 @@ describe('WebIDE utils', () => {
expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
});
+
+ it('returns true if there is a `binary` property already set on the file object', () => {
+ expect(isTextFile({ name: 'abc.txt', content: '' })).toBe(true);
+ expect(isTextFile({ name: 'abc.txt', content: '', binary: true })).toBe(false);
+ });
});
describe('trimPathComponents', () => {
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index f2bfc61381c..0ebe8525b5a 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -85,7 +85,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
+ it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction(
@@ -93,8 +93,8 @@ describe('import_projects store actions', () => {
null,
localState,
[
- { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
@@ -104,19 +104,14 @@ describe('import_projects store actions', () => {
);
});
- it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
+ it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction(
fetchRepos,
null,
localState,
- [
- { type: SET_PAGE, payload: 1 },
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 0 },
- { type: RECEIVE_REPOS_ERROR },
- ],
+ [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
});
@@ -135,7 +130,7 @@ describe('import_projects store actions', () => {
expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
- it('correctly updates current page on an unsuccessful request', () => {
+ it('correctly keeps current page on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
const CURRENT_PAGE = 5;
@@ -143,10 +138,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, pageInfo: { page: CURRENT_PAGE } },
- expect.arrayContaining([
- { type: SET_PAGE, payload: CURRENT_PAGE + 1 },
- { type: SET_PAGE, payload: CURRENT_PAGE },
- ]),
+ expect.arrayContaining([]),
[],
);
});
@@ -159,12 +151,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, filter: 'filter' },
- [
- { type: SET_PAGE, payload: 1 },
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 0 },
- { type: RECEIVE_REPOS_ERROR },
- ],
+ [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
@@ -183,8 +170,8 @@ describe('import_projects store actions', () => {
null,
{ ...localState, filter: 'filter' },
[
- { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 71c22998b08..630d0ffae54 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -1,7 +1,13 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
import TerraformNotification from '~/projects/terraform_notification/components/terraform_notification.vue';
+import {
+ EVENT_LABEL,
+ DISMISS_EVENT,
+ CLICK_EVENT,
+} from '~/projects/terraform_notification/constants';
jest.mock('~/lib/utils/common_utils');
@@ -10,6 +16,7 @@ const bannerDismissedKey = 'terraform_notification_dismissed';
describe('TerraformNotificationBanner', () => {
let wrapper;
+ let trackingSpy;
const provideData = {
terraformImagePath,
@@ -22,11 +29,13 @@ describe('TerraformNotificationBanner', () => {
provide: provideData,
stubs: { GlBanner },
});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
parseBoolean.mockReturnValue(false);
+ unmockTracking();
});
describe('when the dismiss cookie is not set', () => {
@@ -44,8 +53,26 @@ describe('TerraformNotificationBanner', () => {
expect(setCookie).toHaveBeenCalledWith(bannerDismissedKey, true);
});
+ it('should send the dismiss event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
+ label: EVENT_LABEL,
+ });
+ });
+
it('should remove the banner', () => {
expect(findBanner().exists()).toBe(false);
});
});
+
+ describe('when docs link is clicked', () => {
+ beforeEach(async () => {
+ await findBanner().vm.$emit('primary');
+ });
+
+ it('should send button click event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, CLICK_EVENT, {
+ label: EVENT_LABEL,
+ });
+ });
+ });
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 42da1cb71f1..661b1816548 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -267,61 +267,6 @@ RSpec.describe GroupsHelper do
end
end
- describe '#group_sidebar_links' do
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:user) { create(:user) }
-
- before do
- group.add_owner(user)
- allow(helper).to receive(:current_user) { user }
- allow(helper).to receive(:can?) { |*args| Ability.allowed?(*args) }
- helper.instance_variable_set(:@group, group)
- end
-
- it 'returns all the expected links' do
- links = [
- :overview, :activity, :issues, :labels, :milestones, :merge_requests,
- :runners, :group_members, :settings
- ]
-
- expect(helper.group_sidebar_links).to include(*links)
- end
-
- it 'excludes runners when the user cannot admin the group' do
- expect(helper).to receive(:current_user) { user }
- # TODO Proper policies, such as `read_group_runners, should be implemented per
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
- expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
-
- expect(helper.group_sidebar_links).not_to include(:runners)
- end
-
- it 'excludes runners when the feature "runner_list_group_view_vue_ui" is disabled' do
- stub_feature_flags(runner_list_group_view_vue_ui: false)
-
- expect(helper.group_sidebar_links).not_to include(:runners)
- end
-
- it 'excludes settings when the user can admin the group' do
- expect(helper).to receive(:current_user) { user }
- expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
-
- expect(helper.group_sidebar_links).not_to include(:settings)
- end
-
- it 'excludes cross project features when the user cannot read cross project' do
- cross_project_features = [:activity, :issues, :labels, :milestones,
- :merge_requests]
-
- allow(Ability).to receive(:allowed?).and_call_original
- cross_project_features.each do |feature|
- expect(Ability).to receive(:allowed?).with(user, "read_group_#{feature}".to_sym, group) { false }
- end
-
- expect(helper.group_sidebar_links).not_to include(*cross_project_features)
- end
- end
-
describe '#parent_group_options' do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'group') }
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 03b9c538225..79b49be92a5 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has project menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
- title: 'GitLab',
+ title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_project',
title: 'New project/repository',
@@ -117,7 +117,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has group menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
- title: 'GitLab',
+ title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_group',
title: 'New group',
@@ -135,7 +135,7 @@ RSpec.describe Nav::NewDropdownHelper do
it 'has new snippet menu item' do
expect(subject[:menu_sections]).to eq(
expected_menu_section(
- title: 'GitLab',
+ title: _('GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_snippet',
title: 'New snippet',
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
index 5fa414f5bd1..32c92724f62 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb
@@ -3,10 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user, developer_projects: [project]) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+
let(:pipeline) { Ci::Pipeline.new }
- let(:step) { described_class.new(pipeline, command) }
+ let(:bridge) { nil }
+
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -20,7 +26,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
merge_request: nil,
project: project,
current_user: user,
- bridge: bridge)
+ bridge: bridge,
+ variables_attributes: variables_attributes)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ shared_examples 'breaks the chain' do
+ it 'returns true' do
+ step.perform!
+
+ expect(step.break?).to be true
+ end
+ end
+
+ shared_examples 'does not break the chain' do
+ it 'returns false' do
+ step.perform!
+
+ expect(step.break?).to be false
+ end
end
context 'when a bridge is passed in to the pipeline creation' do
@@ -37,26 +62,83 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
)
end
- it 'never breaks the chain' do
- step.perform!
-
- expect(step.break?).to eq(false)
- end
+ it_behaves_like 'does not break the chain'
end
context 'when a bridge is not passed in to the pipeline creation' do
- let(:bridge) { nil }
-
it 'leaves the source pipeline empty' do
step.perform!
expect(pipeline.source_pipeline).to be_nil
end
- it 'never breaks the chain' do
+ it_behaves_like 'does not break the chain'
+ end
+
+ it 'sets pipeline variables' do
+ step.perform!
+
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+
+ context 'when project setting restrict_user_defined_variables is enabled' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ context 'when user is developer' do
+ it_behaves_like 'breaks the chain'
+
+ it 'returns an error on variables_attributes', :aggregate_failures do
+ step.perform!
+
+ expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
+ expect(pipeline.variables).to be_empty
+ end
+
+ context 'when variables_attributes is not specified' do
+ let(:variables_attributes) { nil }
+
+ it_behaves_like 'does not break the chain'
+
+ it 'assigns empty variables' do
+ step.perform!
+
+ expect(pipeline.variables).to be_empty
+ end
+ end
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'does not break the chain'
+
+ it 'assigns variables_attributes' do
+ step.perform!
+
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+ end
+ end
+
+ context 'with duplicate pipeline variables' do
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'first', secret_value: 'second_world' }]
+ end
+
+ it_behaves_like 'breaks the chain'
+
+ it 'returns an error for variables_attributes' do
step.perform!
- expect(step.break?).to eq(false)
+ expect(pipeline.errors.full_messages).to eq(['Duplicate variable name: first'])
+ expect(pipeline.variables).to be_empty
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 7771289abe6..dca2204f544 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -8,11 +8,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
let(:pipeline) { Ci::Pipeline.new }
- let(:variables_attributes) do
- [{ key: 'first', secret_value: 'world' },
- { key: 'second', secret_value: 'second_world' }]
- end
-
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
@@ -24,100 +19,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
schedule: nil,
merge_request: nil,
project: project,
- current_user: user,
- variables_attributes: variables_attributes)
+ current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
- shared_examples 'builds pipeline' do
- it 'builds a pipeline with the expected attributes' do
- step.perform!
-
- expect(pipeline.sha).not_to be_empty
- expect(pipeline.sha).to eq project.commit.id
- expect(pipeline.ref).to eq 'master'
- expect(pipeline.tag).to be false
- expect(pipeline.user).to eq user
- expect(pipeline.project).to eq project
- end
- end
-
- shared_examples 'breaks the chain' do
- it 'returns true' do
- step.perform!
-
- expect(step.break?).to be true
- end
- end
-
- shared_examples 'does not break the chain' do
- it 'returns false' do
- step.perform!
-
- expect(step.break?).to be false
- end
- end
-
- before do
- stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
- end
-
- it_behaves_like 'does not break the chain'
- it_behaves_like 'builds pipeline'
-
- it 'sets pipeline variables' do
+ it 'does not break the chain' do
step.perform!
- expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
- .to eq variables_attributes.map(&:with_indifferent_access)
+ expect(step.break?).to be false
end
- context 'when project setting restrict_user_defined_variables is enabled' do
- before do
- project.update!(restrict_user_defined_variables: true)
- end
+ it 'builds a pipeline with the expected attributes' do
+ step.perform!
- context 'when user is developer' do
- it_behaves_like 'breaks the chain'
- it_behaves_like 'builds pipeline'
-
- it 'returns an error on variables_attributes', :aggregate_failures do
- step.perform!
-
- expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
- expect(pipeline.variables).to be_empty
- end
-
- context 'when variables_attributes is not specified' do
- let(:variables_attributes) { nil }
-
- it_behaves_like 'does not break the chain'
- it_behaves_like 'builds pipeline'
-
- it 'assigns empty variables' do
- step.perform!
-
- expect(pipeline.variables).to be_empty
- end
- end
- end
-
- context 'when user is maintainer' do
- before do
- project.add_maintainer(user)
- end
-
- it_behaves_like 'does not break the chain'
- it_behaves_like 'builds pipeline'
-
- it 'assigns variables_attributes' do
- step.perform!
-
- expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
- .to eq variables_attributes.map(&:with_indifferent_access)
- end
- end
+ expect(pipeline.sha).not_to be_empty
+ expect(pipeline.sha).to eq project.commit.id
+ expect(pipeline.ref).to eq 'master'
+ expect(pipeline.tag).to be false
+ expect(pipeline.user).to eq user
+ expect(pipeline.project).to eq project
end
it 'returns a valid pipeline' do
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index a0fb6a270a5..ca7a6b6b1c3 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -87,8 +87,26 @@ RSpec.describe Gitlab::Tracking::StandardContext do
end
end
- it 'does not contain any ids' do
- expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
+ it 'does not contain user id' do
+ expect(snowplow_context.to_json[:data].keys).not_to include(:user_id)
+ end
+
+ it 'contains namespace and project ids' do
+ expect(snowplow_context.to_json[:data].keys).to include(:project_id, :namespace_id)
+ end
+
+ it 'accepts just project id as integer' do
+ expect { described_class.new(project: 1).to_context }.not_to raise_error
+ end
+
+ context 'without add_namespace_and_project_to_snowplow_tracking feature' do
+ before do
+ stub_feature_flags(add_namespace_and_project_to_snowplow_tracking: false)
+ end
+
+ it 'does not contain any ids' do
+ expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
+ end
end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 994316f38ee..90109db6db2 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do
it "delegates to #{klass} destination" do
other_context = double(:context)
- project = double(:project)
+ project = build_stubbed(:project)
user = double(:user)
expect(Gitlab::Tracking::StandardContext)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 2fdb0ed3c0d..d5d65598589 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1248,16 +1248,47 @@ RSpec.describe Ci::CreatePipelineService do
end
context 'when pipeline variables are specified' do
- let(:variables_attributes) do
- [{ key: 'first', secret_value: 'world' },
- { key: 'second', secret_value: 'second_world' }]
- end
-
subject(:pipeline) { execute_service(variables_attributes: variables_attributes).payload }
- it 'creates a pipeline with specified variables' do
- expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
- .to eq variables_attributes.map(&:with_indifferent_access)
+ context 'with valid pipeline variables' do
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
+
+ it 'creates a pipeline with specified variables' do
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+ end
+
+ context 'with duplicate pipeline variables' do
+ let(:variables_attributes) do
+ [{ key: 'hello', secret_value: 'world' },
+ { key: 'hello', secret_value: 'second_world' }]
+ end
+
+ it 'fails to create the pipeline' do
+ expect(pipeline).to be_failed
+ expect(pipeline.variables).to be_empty
+ expect(pipeline.errors[:base]).to eq(['Duplicate variable name: hello'])
+ end
+ end
+
+ context 'with more than one duplicate pipeline variable' do
+ let(:variables_attributes) do
+ [{ key: 'hello', secret_value: 'world' },
+ { key: 'hello', secret_value: 'second_world' },
+ { key: 'single', secret_value: 'variable' },
+ { key: 'other', secret_value: 'value' },
+ { key: 'other', secret_value: 'other value' }]
+ end
+
+ it 'fails to create the pipeline' do
+ expect(pipeline).to be_failed
+ expect(pipeline.variables).to be_empty
+ expect(pipeline.errors[:base]).to eq(['Duplicate variable names: hello, other'])
+ end
end
end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 2f93b1ecd3c..29d12b0dd0e 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -103,6 +103,17 @@ RSpec.describe Ci::PipelineTriggerService do
end
end
+ context 'when params have duplicate variables' do
+ let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
+ let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
+
+ it 'creates a failed pipeline without variables' do
+ expect { result }.to change { Ci::Pipeline.count }
+ expect(result).to be_error
+ expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
+ end
+ end
+
it_behaves_like 'detecting an unprocessable pipeline trigger'
end
@@ -201,6 +212,17 @@ RSpec.describe Ci::PipelineTriggerService do
end
end
+ context 'when params have duplicate variables' do
+ let(:params) { { token: job.token, ref: 'master', variables: variables } }
+ let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
+
+ it 'creates a failed pipeline without variables' do
+ expect { result }.to change { Ci::Pipeline.count }
+ expect(result).to be_error
+ expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
+ end
+ end
+
it_behaves_like 'detecting an unprocessable pipeline trigger'
end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index a9c6da7bc2b..0ffa32dec9e 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -82,16 +82,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id)
end
- it "does not show already added project" do
- project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
- stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum)
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos")).to eq([])
- end
-
it "touches the etag cache store" do
stub_client(repos: [], orgs: [], each_page: [])
diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
index b9ae0e23e26..44baadaaade 100644
--- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
@@ -19,14 +19,4 @@ RSpec.shared_examples 'import controller status' do
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
end
-
- it "does not show already added project" do
- project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
- stub_client(client_repos_field => [repo])
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos")).to eq([])
- end
end