diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 2a9a08635f7..70d9dbc9ad7 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -147,6 +147,8 @@ /ee/spec/javascripts/ @gitlab-org/maintainers/frontend /spec/frontend/ @gitlab-org/maintainers/frontend /ee/spec/frontend/ @gitlab-org/maintainers/frontend +/spec/frontend_integration/ @gitlab-org/maintainers/frontend +/ee/spec/frontend_integration/ @gitlab-org/maintainers/frontend [Database] /db/ @gitlab-org/maintainers/database diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 5058ca7122d..8f64bda1ba6 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -82,7 +82,6 @@ export default class FileTemplateMediator { initPageEvents() { this.listenForFilenameInput(); - this.prepFileContentForSubmit(); this.listenForPreviewMode(); } @@ -92,12 +91,6 @@ export default class FileTemplateMediator { }); } - prepFileContentForSubmit() { - this.$commitForm.submit(() => { - this.$fileContent.val(this.editor.getValue()); - }); - } - listenForPreviewMode() { this.$navLinks.on('click', 'a', e => { const urlPieces = e.target.href.split('#'); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index c8a31d96a69..1bc51aa1d6f 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -6,6 +6,7 @@ import TemplateSelectorMediator from '../blob/file_template_mediator'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import EditorLite from '~/editor/editor_lite'; import { FileTemplateExtension } from '~/editor/editor_file_template_ext'; +import { insertFinalNewline } from '~/lib/utils/text_utility'; export default class EditBlob { // The options object has: @@ -49,7 +50,7 @@ export default class EditBlob { }); form.addEventListener('submit', () => { - fileContentEl.value = this.editor.getValue(); + fileContentEl.value = insertFinalNewline(this.editor.getValue()); }); } diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 8c72971682d..5b410051705 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -191,7 +191,13 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { commit(types.SET_MERGE_REQUEST_DIFFS, data.merge_request_diffs || []); commit(types.SET_DIFF_METADATA, strippedData); - worker.postMessage(prepareDiffData(data, state.diffFiles)); + worker.postMessage( + prepareDiffData({ + diff: data, + priorFiles: state.diffFiles, + meta: true, + }), + ); return data; }) diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 90940d82226..19122c3096f 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -73,7 +73,10 @@ export default { }, [types.SET_DIFF_DATA_BATCH](state, data) { - const files = prepareDiffData(data, state.diffFiles); + const files = prepareDiffData({ + diff: data, + priorFiles: state.diffFiles, + }); Object.assign(state, { ...convertObjectPropsToCamelCase(data), @@ -143,7 +146,7 @@ export default { }, [types.ADD_COLLAPSED_DIFFS](state, { file, data }) { - const files = prepareDiffData(data); + const files = prepareDiffData({ diff: data }); const [newFileData] = files.filter(f => f.file_hash === file.file_hash); const selectedFile = state.diffFiles.find(f => f.file_hash === file.file_hash); Object.assign(selectedFile, { ...newFileData }); diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 8949c8cd23e..1839df12c96 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -386,9 +386,9 @@ function deduplicateFilesList(files) { return Object.values(dedupedFiles); } -export function prepareDiffData(diff, priorFiles = []) { +export function prepareDiffData({ diff, priorFiles = [], meta = false }) { const cleanedFiles = (diff.diff_files || []) - .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles })) + .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta })) .map(ensureBasicDiffFileLines) .map(prepareDiffFileLines) .map(finalizeDiffFile); diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js index aa801783c57..67f117bc4d3 100644 --- a/app/assets/javascripts/diffs/utils/diff_file.js +++ b/app/assets/javascripts/diffs/utils/diff_file.js @@ -4,6 +4,7 @@ import { DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE, } from '../constants'; +import { uuids } from './uuids'; function fileSymlinkInformation(file, fileList) { const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash); @@ -32,16 +33,29 @@ function collapsed(file) { }; } -export function prepareRawDiffFile({ file, allFiles }) { - Object.assign(file, { +function identifier(file) { + return uuids({ + seeds: [file.file_identifier_hash, file.content_sha], + })[0]; +} + +export function prepareRawDiffFile({ file, allFiles, meta = false }) { + const additionalProperties = { brokenSymlink: fileSymlinkInformation(file, allFiles), viewer: { ...file.viewer, ...collapsed(file), }, - }); + }; - return file; + // It's possible, but not confirmed, that `content_sha` isn't available sometimes + // See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49506#note_464692057 + // We don't want duplicate IDs if that's the case, so we just don't assign an ID + if (!meta && file.content_sha) { + additionalProperties.id = identifier(file); + } + + return Object.assign(file, additionalProperties); } export function collapsedType(file) { diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index c5bb00c3dee..2471b3627ce 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,7 +1,8 @@ import { editor as monacoEditor, Uri } from 'monaco-editor'; import Disposable from './disposable'; import eventHub from '../../eventhub'; -import { trimTrailingWhitespace, insertFinalNewline } from '../../utils'; +import { trimTrailingWhitespace } from '../../utils'; +import { insertFinalNewline } from '~/lib/utils/text_utility'; import { defaultModelOptions } from '../editor_options'; export default class Model { diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 1ca1b971de1..43276f32322 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -97,10 +97,6 @@ export function trimTrailingWhitespace(content) { return content.replace(/[^\S\r\n]+$/gm, ''); } -export function insertFinalNewline(content, eol = '\n') { - return content.slice(-eol.length) !== eol ? `${content}${eol}` : content; -} - export function getPathParents(path, maxDepth = Infinity) { const pathComponents = path.split('/'); const paths = []; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index a81ca3f211f..c398874db24 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -411,3 +411,13 @@ export const hasContent = obj => isString(obj) && obj.trim() !== ''; export const isValidSha1Hash = str => { return /^[0-9a-f]{5,40}$/.test(str); }; + +/** + * Adds a final newline to the content if it doesn't already exist + * + * @param {*} content Content + * @param {*} endOfLine Type of newline: CRLF='\r\n', LF='\n', CR='\r' + */ +export function insertFinalNewline(content, endOfLine = '\n') { + return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content; +} diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 8f836010d70..d1b09e1b49e 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController before_action :authorize_admin_group_member!, except: admin_not_required_endpoints before_action do - push_frontend_feature_flag(:group_members_filtered_search, @group) + push_frontend_feature_flag(:group_members_filtered_search, @group, default_enabled: true) end skip_before_action :check_two_factor_requirement, only: :leave diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 2da5832a1ef..705e0c136ef 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -57,7 +57,7 @@ class Import::BulkImportsController < ApplicationController end def create_params - params.permit(:bulk_import, [*bulk_import_params]) + params.permit(bulk_import: bulk_import_params)[:bulk_import] end def bulk_import_params @@ -127,7 +127,7 @@ class Import::BulkImportsController < ApplicationController def credentials { url: session[url_key], - access_token: [access_token_key] + access_token: session[access_token_key] } end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 6f063c607b3..d23e66b9697 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -745,31 +745,7 @@ class MergeRequestDiff < ApplicationRecord def sort_diffs(diffs) return diffs unless sort_diffs? - diffs.sort do |a, b| - compare_path_parts(path_parts(a), path_parts(b)) - end - end - - def path_parts(diff) - (diff.new_path.presence || diff.old_path).split(::File::SEPARATOR) - end - - # Used for sorting the file paths by: - # 1. Directory name - # 2. Depth - # 3. File name - def compare_path_parts(a_parts, b_parts) - a_part = a_parts.shift - b_part = b_parts.shift - - return 1 if a_parts.size < b_parts.size && a_parts.empty? - return -1 if a_parts.size > b_parts.size && b_parts.empty? - - comparison = a_part <=> b_part - - return comparison unless comparison == 0 - - compare_path_parts(a_parts, b_parts) + Gitlab::Diff::FileCollectionSorter.new(diffs).sort end def sort_diffs? diff --git a/app/models/raw_usage_data.rb b/app/models/raw_usage_data.rb index 18cee55d06e..06cd4ad3f6c 100644 --- a/app/models/raw_usage_data.rb +++ b/app/models/raw_usage_data.rb @@ -5,6 +5,6 @@ class RawUsageData < ApplicationRecord validates :recorded_at, presence: true, uniqueness: true def update_sent_at! - self.update_column(:sent_at, Time.current) if Feature.enabled?(:save_raw_usage_data) + self.update_column(:sent_at, Time.current) end end diff --git a/app/serializers/diffs_metadata_entity.rb b/app/serializers/diffs_metadata_entity.rb index 8973f23734a..7b0de3bce4e 100644 --- a/app/serializers/diffs_metadata_entity.rb +++ b/app/serializers/diffs_metadata_entity.rb @@ -2,7 +2,9 @@ class DiffsMetadataEntity < DiffsEntity unexpose :diff_files - expose :raw_diff_files, as: :diff_files, using: DiffFileMetadataEntity + expose :diff_files, using: DiffFileMetadataEntity do |diffs, _| + diffs.raw_diff_files(sorted: true) + end expose :conflict_resolution_path do |_, options| presenter(options[:merge_request]).conflict_resolution_path diff --git a/app/serializers/import/bulk_import_entity.rb b/app/serializers/import/bulk_import_entity.rb index 8f0a9dd4428..9daa6699a20 100644 --- a/app/serializers/import/bulk_import_entity.rb +++ b/app/serializers/import/bulk_import_entity.rb @@ -12,4 +12,8 @@ class Import::BulkImportEntity < Grape::Entity expose :full_path do |entity| entity['full_path'] end + + expose :web_url do |entity| + entity['web_url'] + end end diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb index fe59686278c..1118b1aa4fe 100644 --- a/app/serializers/paginated_diff_entity.rb +++ b/app/serializers/paginated_diff_entity.rb @@ -13,7 +13,7 @@ class PaginatedDiffEntity < Grape::Entity submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) DiffFileEntity.represent( - diffs.diff_files, + diffs.diff_files(sorted: true), options.merge( submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs), diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 2fbeaf4405c..8ab1193b04f 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -43,8 +43,6 @@ class SubmitUsagePingService private def save_raw_usage_data(usage_data) - return unless Feature.enabled?(:save_raw_usage_data) - RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record| record.payload = usage_data end diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a08212f151c..a1527a74898 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -4,7 +4,7 @@ - show_access_requests = can_manage_members && @requesters.exists? - invited_active = params[:search_invited].present? || params[:invited_members_page].present? - vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true) -- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group) +- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group, default_enabled: true) - current_user_is_group_owner = @group && @group.has_owner?(current_user) - form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center' diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 7679e0714fe..9d4e5d629f4 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -27,9 +27,6 @@ = sprite_icon("rocket", size: 12) = _("Release") = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'gl-text-blue-600!' - - if release.description.present? - .md.gl-mt-3 - = markdown_field(release, :description) .row-fixed-content.controls.flex-row - if tag.has_signature? diff --git a/changelogs/unreleased/219852-deprecated-button.yml b/changelogs/unreleased/219852-deprecated-button.yml new file mode 100644 index 00000000000..46a9f556c4e --- /dev/null +++ b/changelogs/unreleased/219852-deprecated-button.yml @@ -0,0 +1,5 @@ +--- +title: Update deprecated button on pipeline security table +merge_request: 49620 +author: +type: changed diff --git a/changelogs/unreleased/241158-remove-feature-flag-save_raw_usage_data.yml b/changelogs/unreleased/241158-remove-feature-flag-save_raw_usage_data.yml new file mode 100644 index 00000000000..0f07284dcca --- /dev/null +++ b/changelogs/unreleased/241158-remove-feature-flag-save_raw_usage_data.yml @@ -0,0 +1,5 @@ +--- +title: Save usage ping payload in raw_usage_data table +merge_request: 49559 +author: +type: added diff --git a/changelogs/unreleased/26552-sort-compare-commit.yml b/changelogs/unreleased/26552-sort-compare-commit.yml new file mode 100644 index 00000000000..7d5295acf1f --- /dev/null +++ b/changelogs/unreleased/26552-sort-compare-commit.yml @@ -0,0 +1,5 @@ +--- +title: Sort commit/compare diff files directory first +merge_request: 49136 +author: +type: changed diff --git a/changelogs/unreleased/267514-trailing-newline.yml b/changelogs/unreleased/267514-trailing-newline.yml new file mode 100644 index 00000000000..550000e930e --- /dev/null +++ b/changelogs/unreleased/267514-trailing-newline.yml @@ -0,0 +1,5 @@ +--- +title: Add final newline on submit in blob editor +merge_request: 49681 +author: +type: fixed diff --git a/changelogs/unreleased/289911-feature-flag-rollout-of-group_members_filtered_search.yml b/changelogs/unreleased/289911-feature-flag-rollout-of-group_members_filtered_search.yml new file mode 100644 index 00000000000..56346d6d586 --- /dev/null +++ b/changelogs/unreleased/289911-feature-flag-rollout-of-group_members_filtered_search.yml @@ -0,0 +1,5 @@ +--- +title: Convert group member filter dropdowns to filtered search bar +merge_request: 49505 +author: +type: changed diff --git a/changelogs/unreleased/nfriend-remove-release-notes-from-tags-page.yml b/changelogs/unreleased/nfriend-remove-release-notes-from-tags-page.yml new file mode 100644 index 00000000000..9ebbb3b3209 --- /dev/null +++ b/changelogs/unreleased/nfriend-remove-release-notes-from-tags-page.yml @@ -0,0 +1,5 @@ +--- +title: Remove release notes from Tags page +merge_request: 49979 +author: +type: removed diff --git a/config/feature_flags/development/group_members_filtered_search.yml b/config/feature_flags/development/group_members_filtered_search.yml index ea1a5b6a74f..8a30bdd3d92 100644 --- a/config/feature_flags/development/group_members_filtered_search.yml +++ b/config/feature_flags/development/group_members_filtered_search.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/289911 milestone: '13.7' type: development group: group::access -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/save_raw_usage_data.yml b/config/feature_flags/development/save_raw_usage_data.yml deleted file mode 100644 index 44820fe2f53..00000000000 --- a/config/feature_flags/development/save_raw_usage_data.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: save_raw_usage_data -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38457 -rollout_issue_url: -milestone: '13.3' -type: development -group: group::product analytics -default_enabled: false diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 754f4d7cca3..1246c425879 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -4121,6 +4121,56 @@ type CreateClusterAgentPayload { errors: [String!]! } +""" +Autogenerated input type of CreateComplianceFramework +""" +input CreateComplianceFrameworkInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123. + """ + color: String! + + """ + Description of the compliance framework. + """ + description: String! + + """ + Name of the compliance framework. + """ + name: String! + + """ + Full path of the namespace to add the compliance framework to. + """ + namespacePath: ID! +} + +""" +Autogenerated return type of CreateComplianceFramework +""" +type CreateComplianceFrameworkPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + The created compliance framework. + """ + framework: ComplianceFramework +} + """ Autogenerated input type of CreateCustomEmoji """ @@ -14545,6 +14595,7 @@ type Mutation { createBoard(input: CreateBoardInput!): CreateBoardPayload createBranch(input: CreateBranchInput!): CreateBranchPayload createClusterAgent(input: CreateClusterAgentInput!): CreateClusterAgentPayload + createComplianceFramework(input: CreateComplianceFrameworkInput!): CreateComplianceFrameworkPayload """ Available only when feature flag `custom_emoji` is enabled. diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f0a99a7e522..10709f664f6 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -11320,6 +11320,150 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateComplianceFrameworkInput", + "description": "Autogenerated input type of CreateComplianceFramework", + "fields": null, + "inputFields": [ + { + "name": "namespacePath", + "description": "Full path of the namespace to add the compliance framework to.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "Name of the compliance framework.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the compliance framework.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "color", + "description": "Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateComplianceFrameworkPayload", + "description": "Autogenerated return type of CreateComplianceFramework", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "framework", + "description": "The created compliance framework.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ComplianceFramework", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateCustomEmojiInput", @@ -40796,6 +40940,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createComplianceFramework", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateComplianceFrameworkInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateComplianceFrameworkPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createCustomEmoji", "description": " Available only when feature flag `custom_emoji` is enabled.", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cba68ceac3b..f267a190d45 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -693,6 +693,16 @@ Autogenerated return type of CreateClusterAgent. | `clusterAgent` | ClusterAgent | Cluster agent created after mutation | | `errors` | String! => Array | Errors encountered during execution of the mutation. | +### CreateComplianceFrameworkPayload + +Autogenerated return type of CreateComplianceFramework. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `framework` | ComplianceFramework | The created compliance framework. | + ### CreateCustomEmojiPayload Autogenerated return type of CreateCustomEmoji. diff --git a/doc/ci/img/junit_test_report.png b/doc/ci/img/junit_test_report.png index ad098eb457f..a4b98c8b910 100644 Binary files a/doc/ci/img/junit_test_report.png and b/doc/ci/img/junit_test_report.png differ diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merged_result_pipeline.png b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merged_result_pipeline.png new file mode 100644 index 00000000000..2584cd4d38d Binary files /dev/null and b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/img/merged_result_pipeline.png differ diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index e154d8c5a72..1b9bade3b76 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -15,7 +15,9 @@ source branch into a target branch. By default, the CI pipeline runs jobs against the source branch. With *pipelines for merged results*, the pipeline runs as if the changes from -the source branch have already been merged into the target branch. +the source branch have already been merged into the target branch. The commit shown for the pipeline does not exist on the source or target branches but represents the combined target and source branches. + +![Merge request widget for merged results pipeline](img/merged_result_pipeline.png) If the pipeline fails due to a problem in the target branch, you can wait until the target is fixed and re-run the pipeline. diff --git a/doc/ci/unit_test_reports.md b/doc/ci/unit_test_reports.md index e421ea5c2eb..2505e56356d 100644 --- a/doc/ci/unit_test_reports.md +++ b/doc/ci/unit_test_reports.md @@ -65,6 +65,39 @@ execution time and the error output. ![Test Reports Widget](img/junit_test_report.png) +### Number of recent failures + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241759) in GitLab 13.7. +> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default. +> - It's disabled on GitLab.com. +> - It's not recommended for production use. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-the-number-of-recent-failures). **(CORE ONLY)** + +WARNING: +This feature might not be available to you. Check the **version history** note above for details. + +If a test failed in the project's default branch in the last 14 days, a message like +`Failed {n} time(s) in {default_branch} in the last 14 days` is displayed for that test. + +#### Enable or disable the number of recent failures **(CORE ONLY)** + +Displaying the number of failures in the last 14 days is under development and not +ready for production use. It is deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) +can enable it. + +To enable it: + +```ruby +Feature.enable(:test_failure_history) +``` + +To disable it: + +```ruby +Feature.disable(:test_failure_history) +``` + ## How to set it up To enable the Unit test reports in merge requests, you need to add diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 7a94e03ac5a..f9e63d6b00b 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -654,6 +654,17 @@ When the docs are generated, the output is: To stop the command, press Control+C. +### Spaces between words + +Use only standard spaces between words. The search engine for the documentation +website doesn't split words separated with +[non-breaking spaces](https://en.wikipedia.org/wiki/Non-breaking_space) when +indexing, and fails to create expected individual search terms. Tests that search +for certain words separated by regular spaces can't find words separated by +non-breaking spaces. + +Tested in [`lint-doc.sh`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/lint-doc.sh). + ## Lists - Always start list items with a capital letter, unless they're parameters or diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index e587316bd3b..c4f1bd8b424 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -74,7 +74,7 @@ which means that the reported licenses might be incomplete or inaccurate. | JavaScript | [Yarn](https://yarnpkg.com/)|[License Finder](https://github.com/pivotal/LicenseFinder)| | Go | go get, gvt, glide, dep, trash, govendor |[License Finder](https://github.com/pivotal/LicenseFinder)| | Erlang | [Rebar](https://www.rebar3.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)| -| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) | | [License Finder](https://github.com/pivotal/LicenseFinder) | +| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) | [License Finder](https://github.com/pivotal/LicenseFinder) | | Objective-C, Swift | [CocoaPods](https://cocoapods.org/) v0.39 and below |[License Finder](https://github.com/pivotal/LicenseFinder)| | Elixir | [Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) |[License Finder](https://github.com/pivotal/LicenseFinder)| | C++/C | [Conan](https://conan.io/) |[License Finder](https://github.com/pivotal/LicenseFinder)| diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index cf0611e44da..8f4f8febec0 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -30,12 +30,16 @@ module Gitlab @diffs ||= diffable.raw_diffs(diff_options) end - def diff_files - raw_diff_files + def diff_files(sorted: false) + raw_diff_files(sorted: sorted) end - def raw_diff_files - @raw_diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) } + def raw_diff_files(sorted: false) + strong_memoize(:"raw_diff_files_#{sorted}") do + collection = diffs.decorate! { |diff| decorate_diff!(diff) } + collection = sort_diffs(collection) if sorted + collection + end end def diff_file_paths @@ -111,6 +115,12 @@ module Gitlab fallback_diff_refs: fallback_diff_refs, stats: stats) end + + def sort_diffs(diffs) + return diffs unless Feature.enabled?(:sort_diffs, project, default_enabled: false) + + Gitlab::Diff::FileCollectionSorter.new(diffs).sort + end end end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb index 16257bb5ff5..d2ca86fdfe7 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb @@ -16,7 +16,7 @@ module Gitlab fallback_diff_refs: merge_request_diff.fallback_diff_refs) end - def diff_files + def diff_files(sorted: false) strong_memoize(:diff_files) do diff_files = super @@ -26,6 +26,12 @@ module Gitlab end end + def raw_diff_files(sorted: false) + # We force `sorted` to `false` as we don't need to sort the diffs when + # dealing with `MergeRequestDiff` since we sort its files on create. + super(sorted: false) + end + override :write_cache def write_cache highlight_cache.write_if_empty diff --git a/lib/gitlab/diff/file_collection_sorter.rb b/lib/gitlab/diff/file_collection_sorter.rb new file mode 100644 index 00000000000..94626875580 --- /dev/null +++ b/lib/gitlab/diff/file_collection_sorter.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class FileCollectionSorter + attr_reader :diffs + + def initialize(diffs) + @diffs = diffs + end + + def sort + diffs.sort do |a, b| + compare_path_parts(path_parts(a), path_parts(b)) + end + end + + private + + def path_parts(diff) + (diff.new_path.presence || diff.old_path).split(::File::SEPARATOR) + end + + # Used for sorting the file paths by: + # 1. Directory name + # 2. Depth + # 3. File name + def compare_path_parts(a_parts, b_parts) + a_part = a_parts.shift + b_part = b_parts.shift + + return 1 if a_parts.size < b_parts.size && a_parts.empty? + return -1 if a_parts.size > b_parts.size && b_parts.empty? + + comparison = a_part <=> b_part + + return comparison unless comparison == 0 + + compare_path_parts(a_parts, b_parts) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f0336bb3af6..7444c07bd3a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11805,9 +11805,6 @@ msgstr "" msgid "Feature flag was successfully removed." msgstr "" -msgid "Feature not available" -msgstr "" - msgid "FeatureFlags|%d user" msgid_plural "FeatureFlags|%d users" msgstr[0] "" diff --git a/rubocop/cop/graphql/id_type.rb b/rubocop/cop/graphql/id_type.rb index 96f90ac136a..0d2fb6ad852 100644 --- a/rubocop/cop/graphql/id_type.rb +++ b/rubocop/cop/graphql/id_type.rb @@ -6,7 +6,7 @@ module RuboCop class IDType < RuboCop::Cop::Cop MSG = 'Do not use GraphQL::ID_TYPE, use a specific GlobalIDType instead' - WHITELISTED_ARGUMENTS = %i[iid full_path project_path group_path target_project_path].freeze + WHITELISTED_ARGUMENTS = %i[iid full_path project_path group_path target_project_path namespace_path].freeze def_node_search :graphql_id_type?, <<~PATTERN (send nil? :argument (_ #does_not_match?) (const (const nil? :GraphQL) :ID_TYPE) ...) diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 7dac15c6314..23e7cb6c455 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -18,6 +18,21 @@ then ((ERRORCODE++)) fi +# Test for non-standard spaces (NBSP, NNBSP) in documentation. +echo '=> Checking for non-standard spaces...' +echo +grep --extended-regexp --binary-file=without-match --recursive '[ ]' doc/ >/dev/null 2>&1 +if [ $? -eq 0 ] +then + echo '✖ ERROR: Non-standard spaces (NBSP, NNBSP) should not be used in documentation. + https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#spaces-between-words + Replace with standard spaces:' >&2 + # Find the spaces, then add color codes with sed to highlight each NBSP or NNBSP in the output. + grep --extended-regexp --binary-file=without-match --recursive --color=auto '[ ]' doc \ + | sed -e ''/ /s//`printf "\033[0;101m \033[0m"`/'' -e ''/ /s//`printf "\033[0;101m \033[0m"`/'' + ((ERRORCODE++)) +fi + # Ensure that the CHANGELOG.md does not contain duplicate versions DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d) echo '=> Checking for CHANGELOG.md duplicate entries...' diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb index dd850a86227..436daed0af6 100644 --- a/spec/controllers/import/bulk_imports_controller_spec.rb +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -57,8 +57,8 @@ RSpec.describe Import::BulkImportsController do let(:client_response) do double( parsed_response: [ - { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1' }, - { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2' } + { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1', 'web_url' => 'http://demo.host/full/path/group1' }, + { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2', 'web_url' => 'http://demo.host/full/path/group1' } ] ) end @@ -132,12 +132,27 @@ RSpec.describe Import::BulkImportsController do end describe 'POST create' do + let(:instance_url) { "http://fake-intance" } + let(:pat) { "fake-pat" } + + before do + session[:bulk_import_gitlab_access_token] = pat + session[:bulk_import_gitlab_url] = instance_url + end + it 'executes BulkImportService' do - expect_next_instance_of(BulkImportService) do |service| + bulk_import_params = [{ "source_type" => "group_entity", + "source_full_path" => "full_path", + "destination_name" => + "destination_name", + "destination_namespace" => "root" }] + + expect_next_instance_of( + BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service| expect(service).to receive(:execute) end - post :create + post :create, params: { bulk_import: bulk_import_params } expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index 0ac379e03c9..9637ea09a3a 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -12,13 +12,18 @@ describe('Blob Editing', () => { const useMock = jest.fn(); const mockInstance = { use: useMock, - getValue: jest.fn(), + setValue: jest.fn(), + getValue: jest.fn().mockReturnValue('test value'), focus: jest.fn(), }; beforeEach(() => { - setFixtures( - `