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( - `
`, - ); + setFixtures(` +
+
+
+ +
+ `); jest.spyOn(EditorLite.prototype, 'createInstance').mockReturnValue(mockInstance); }); afterEach(() => { @@ -55,4 +60,15 @@ describe('Blob Editing', () => { expect(EditorMarkdownExtension).toHaveBeenCalledTimes(1); }); }); + + it('adds trailing newline to the blob content on submit', async () => { + const form = document.querySelector('.js-edit-blob-form'); + const fileContentEl = document.getElementById('file-content'); + + await initEditor(); + + form.dispatchEvent(new Event('submit')); + + expect(fileContentEl.value).toBe('test value\n'); + }); }); diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index a78c4c2d065..7ee97224707 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -409,10 +409,13 @@ describe('DiffsStoreUtils', () => { diff_files: [{ ...mock, [INLINE_DIFF_LINES_KEY]: undefined }], }; - preparedDiff.diff_files = utils.prepareDiffData(preparedDiff); - splitInlineDiff.diff_files = utils.prepareDiffData(splitInlineDiff); - splitParallelDiff.diff_files = utils.prepareDiffData(splitParallelDiff); - completedDiff.diff_files = utils.prepareDiffData(completedDiff, [mock]); + preparedDiff.diff_files = utils.prepareDiffData({ diff: preparedDiff }); + splitInlineDiff.diff_files = utils.prepareDiffData({ diff: splitInlineDiff }); + splitParallelDiff.diff_files = utils.prepareDiffData({ diff: splitParallelDiff }); + completedDiff.diff_files = utils.prepareDiffData({ + diff: completedDiff, + priorFiles: [mock], + }); }); it('sets the renderIt and collapsed attribute on files', () => { @@ -447,7 +450,10 @@ describe('DiffsStoreUtils', () => { content_sha: 'ABC', file_hash: 'DEF', }; - const updatedFilesList = utils.prepareDiffData({ diff_files: [fakeNewFile] }, priorFiles); + const updatedFilesList = utils.prepareDiffData({ + diff: { diff_files: [fakeNewFile] }, + priorFiles, + }); expect(updatedFilesList).toEqual([mock, fakeNewFile]); }); @@ -460,7 +466,10 @@ describe('DiffsStoreUtils', () => { { ...mock, [INLINE_DIFF_LINES_KEY]: undefined }, { ...mock, [INLINE_DIFF_LINES_KEY]: undefined, content_sha: 'ABC', file_hash: 'DEF' }, ]; - const updatedFilesList = utils.prepareDiffData({ diff_files: fakeBatch }, priorFiles); + const updatedFilesList = utils.prepareDiffData({ + diff: { diff_files: fakeBatch }, + priorFiles, + }); expect(updatedFilesList).toEqual([ mock, @@ -498,7 +507,7 @@ describe('DiffsStoreUtils', () => { beforeEach(() => { mock = getDiffMetadataMock(); - preparedDiffFiles = utils.prepareDiffData(mock); + preparedDiffFiles = utils.prepareDiffData({ diff: mock, meta: true }); }); it('sets the renderIt and collapsed attribute on files', () => { @@ -514,7 +523,7 @@ describe('DiffsStoreUtils', () => { const fileMock = getDiffFileMock(); const metaData = getDiffMetadataMock(); const priorFiles = [fileMock]; - const updatedFilesList = utils.prepareDiffData(metaData, priorFiles); + const updatedFilesList = utils.prepareDiffData({ diff: metaData, priorFiles, meta: true }); expect(updatedFilesList.length).toEqual(2); expect(updatedFilesList[0]).toEqual(fileMock); @@ -539,7 +548,7 @@ describe('DiffsStoreUtils', () => { const fileMock = getDiffFileMock(); const metaMock = getDiffMetadataMock(); const priorFiles = [{ ...fileMock }]; - const updatedFilesList = utils.prepareDiffData(metaMock, priorFiles); + const updatedFilesList = utils.prepareDiffData({ diff: metaMock, priorFiles, meta: true }); expect(updatedFilesList).toEqual([ fileMock, diff --git a/spec/frontend/diffs/utils/diff_file_spec.js b/spec/frontend/diffs/utils/diff_file_spec.js index dfa63e51778..815975f45cf 100644 --- a/spec/frontend/diffs/utils/diff_file_spec.js +++ b/spec/frontend/diffs/utils/diff_file_spec.js @@ -1,31 +1,42 @@ import { prepareRawDiffFile } from '~/diffs/utils/diff_file'; -const DIFF_FILES = [ - { - file_hash: 'ABC', // This file is just a normal file - }, - { - file_hash: 'DEF', // This file replaces a symlink - a_mode: '0', - b_mode: '0755', - }, - { - file_hash: 'DEF', // This symlink is replaced by a file - a_mode: '120000', - b_mode: '0', - }, - { - file_hash: 'GHI', // This symlink replaces a file - a_mode: '0', - b_mode: '120000', - }, - { - file_hash: 'GHI', // This file is replaced by a symlink - a_mode: '0755', - b_mode: '0', - }, -]; - +function getDiffFiles() { + return [ + { + file_hash: 'ABC', // This file is just a normal file + file_identifier_hash: 'ABC1', + content_sha: 'C047347', + }, + { + file_hash: 'DEF', // This file replaces a symlink + file_identifier_hash: 'DEF1', + content_sha: 'C047347', + a_mode: '0', + b_mode: '0755', + }, + { + file_hash: 'DEF', // This symlink is replaced by a file + file_identifier_hash: 'DEF2', + content_sha: 'C047347', + a_mode: '120000', + b_mode: '0', + }, + { + file_hash: 'GHI', // This symlink replaces a file + file_identifier_hash: 'GHI1', + content_sha: 'C047347', + a_mode: '0', + b_mode: '120000', + }, + { + file_hash: 'GHI', // This file is replaced by a symlink + file_identifier_hash: 'GHI2', + content_sha: 'C047347', + a_mode: '0755', + b_mode: '0', + }, + ]; +} function makeBrokenSymlinkObject(replaced, wasSymbolic, isSymbolic, wasReal, isReal) { return { replaced, @@ -38,6 +49,12 @@ function makeBrokenSymlinkObject(replaced, wasSymbolic, isSymbolic, wasReal, isR describe('diff_file utilities', () => { describe('prepareRawDiffFile', () => { + let files; + + beforeEach(() => { + files = getDiffFiles(); + }); + it.each` fileIndex | description | brokenSymlink ${0} | ${'a file that is not symlink-adjacent'} | ${false} @@ -49,12 +66,51 @@ describe('diff_file utilities', () => { 'properly marks $description with the correct .brokenSymlink value', ({ fileIndex, brokenSymlink }) => { const preppedRaw = prepareRawDiffFile({ - file: DIFF_FILES[fileIndex], - allFiles: DIFF_FILES, + file: files[fileIndex], + allFiles: files, }); expect(preppedRaw.brokenSymlink).toStrictEqual(brokenSymlink); }, ); + + it.each` + fileIndex | id + ${0} | ${'e075da30-4ec7-4e1c-a505-fe0fb0efe2d8'} + ${1} | ${'5ab05419-123e-4d18-8454-0b8c3d9f3f91'} + ${2} | ${'94eb6bba-575c-4504-bd8e-5d302364d31e'} + ${3} | ${'06d669b2-29b7-4f47-9731-33fc38a8db61'} + ${4} | ${'edd3e8f9-07f9-4647-8171-544c72e5a175'} + `('sets the file id properly { id: $id } on normal diff files', ({ fileIndex, id }) => { + const preppedFile = prepareRawDiffFile({ + file: files[fileIndex], + allFiles: files, + }); + + expect(preppedFile.id).toBe(id); + }); + + it('does not set the `id` property for metadata diff files', () => { + const preppedFile = prepareRawDiffFile({ + file: files[0], + allFiles: files, + meta: true, + }); + + expect(preppedFile).not.toHaveProp('id'); + }); + + it('does not set the id property if the file is missing a `content_sha`', () => { + const fileMissingContentSha = { ...files[0] }; + + delete fileMissingContentSha.content_sha; + + const preppedFile = prepareRawDiffFile({ + file: fileMissingContentSha, + allFiles: files, + }); + + expect(preppedFile).not.toHaveProp('id'); + }); }); }); diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index 6cd2128d356..3b772c0b259 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -4,7 +4,6 @@ import { registerLanguages, registerSchema, trimPathComponents, - insertFinalNewline, trimTrailingWhitespace, getPathParents, getPathParent, @@ -225,29 +224,6 @@ describe('WebIDE utils', () => { }); }); - describe('addFinalNewline', () => { - it.each` - input | output - ${'some text'} | ${'some text\n'} - ${'some text\n'} | ${'some text\n'} - ${'some text\n\n'} | ${'some text\n\n'} - ${'some\n text'} | ${'some\n text\n'} - `('adds a newline if it doesnt already exist for input: $input', ({ input, output }) => { - expect(insertFinalNewline(input)).toBe(output); - }); - - it.each` - input | output - ${'some text'} | ${'some text\r\n'} - ${'some text\r\n'} | ${'some text\r\n'} - ${'some text\n'} | ${'some text\n\r\n'} - ${'some text\r\n\r\n'} | ${'some text\r\n\r\n'} - ${'some\r\n text'} | ${'some\r\n text\r\n'} - `('works with CRLF newline style; input: $input', ({ input, output }) => { - expect(insertFinalNewline(input, '\r\n')).toBe(output); - }); - }); - describe('getPathParents', () => { it.each` path | parents diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index d7cedb939d2..9c50bf577dc 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -340,4 +340,27 @@ describe('text_utility', () => { expect(textUtils.isValidSha1Hash(hash)).toBe(valid); }); }); + + describe('insertFinalNewline', () => { + it.each` + input | output + ${'some text'} | ${'some text\n'} + ${'some text\n'} | ${'some text\n'} + ${'some text\n\n'} | ${'some text\n\n'} + ${'some\n text'} | ${'some\n text\n'} + `('adds a newline if it doesnt already exist for input: $input', ({ input, output }) => { + expect(textUtils.insertFinalNewline(input)).toBe(output); + }); + + it.each` + input | output + ${'some text'} | ${'some text\r\n'} + ${'some text\r\n'} | ${'some text\r\n'} + ${'some text\n'} | ${'some text\n\r\n'} + ${'some text\r\n\r\n'} | ${'some text\r\n\r\n'} + ${'some\r\n text'} | ${'some\r\n text\r\n'} + `('works with CRLF newline style; input: $input', ({ input, output }) => { + expect(textUtils.insertFinalNewline(input, '\r\n')).toBe(output); + }); + }); }); diff --git a/spec/lib/gitlab/diff/file_collection/commit_spec.rb b/spec/lib/gitlab/diff/file_collection/commit_spec.rb index 7773604a638..3d995b36b6f 100644 --- a/spec/lib/gitlab/diff/file_collection/commit_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/commit_spec.rb @@ -4,17 +4,75 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::FileCollection::Commit do let(:project) { create(:project, :repository) } + let(:diffable) { project.commit } + + let(:collection_default_args) do + { diff_options: {} } + end it_behaves_like 'diff statistics' do - let(:collection_default_args) do - { diff_options: {} } - end - - let(:diffable) { project.commit } let(:stub_path) { 'bar/branch-test.txt' } end - it_behaves_like 'unfoldable diff' do - let(:diffable) { project.commit } + it_behaves_like 'unfoldable diff' + + it_behaves_like 'sortable diff files' do + let(:diffable) { project.commit('913c66a') } + + let(:unsorted_diff_files_paths) do + [ + '.DS_Store', + 'CHANGELOG', + 'MAINTENANCE.md', + 'PROCESS.md', + 'VERSION', + 'encoding/feature-1.txt', + 'encoding/feature-2.txt', + 'encoding/hotfix-1.txt', + 'encoding/hotfix-2.txt', + 'encoding/russian.rb', + 'encoding/test.txt', + 'encoding/テスト.txt', + 'encoding/テスト.xls', + 'files/.DS_Store', + 'files/html/500.html', + 'files/images/logo-black.png', + 'files/images/logo-white.png', + 'files/js/application.js', + 'files/js/commit.js.coffee', + 'files/markdown/ruby-style-guide.md', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb' + ] + end + + let(:sorted_diff_files_paths) do + [ + 'encoding/feature-1.txt', + 'encoding/feature-2.txt', + 'encoding/hotfix-1.txt', + 'encoding/hotfix-2.txt', + 'encoding/russian.rb', + 'encoding/test.txt', + 'encoding/テスト.txt', + 'encoding/テスト.xls', + 'files/html/500.html', + 'files/images/logo-black.png', + 'files/images/logo-white.png', + 'files/js/application.js', + 'files/js/commit.js.coffee', + 'files/markdown/ruby-style-guide.md', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'files/.DS_Store', + '.DS_Store', + 'CHANGELOG', + 'MAINTENANCE.md', + 'PROCESS.md', + 'VERSION' + ] + end end end diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb index dda4513a3a1..f3326f4f03d 100644 --- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb @@ -27,4 +27,43 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do let(:diffable) { Compare.new(raw_compare, project) } let(:stub_path) { '.gitignore' } end + + it_behaves_like 'sortable diff files' do + let(:diffable) { Compare.new(raw_compare, project) } + let(:collection_default_args) do + { + project: diffable.project, + diff_options: {}, + diff_refs: diffable.diff_refs + } + end + + let(:unsorted_diff_files_paths) do + [ + '.DS_Store', + '.gitignore', + '.gitmodules', + 'Gemfile.zip', + 'files/.DS_Store', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'gitlab-shell' + ] + end + + let(:sorted_diff_files_paths) do + [ + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'files/.DS_Store', + '.DS_Store', + '.gitignore', + '.gitmodules', + 'Gemfile.zip', + 'gitlab-shell' + ] + end + end end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb index 959afb98fd1..670c734ce08 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb @@ -144,4 +144,18 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do it_behaves_like 'cacheable diff collection' do let(:cacheable_files_count) { batch_size } end + + it_behaves_like 'unsortable diff files' do + let(:diffable) { merge_request.merge_request_diff } + let(:collection_default_args) do + { diff_options: {} } + end + + subject do + described_class.new(merge_request.merge_request_diff, + batch_page, + batch_size, + **collection_default_args) + end + end end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 429e552278d..03a9b9bd21e 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -54,4 +54,11 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiff do it 'returns a valid instance of a DiffCollection' do expect(diff_files).to be_a(Gitlab::Git::DiffCollection) end + + it_behaves_like 'unsortable diff files' do + let(:diffable) { merge_request.merge_request_diff } + let(:collection_default_args) do + { diff_options: {} } + end + end end diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb new file mode 100644 index 00000000000..8822fc55c6e --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Diff::FileCollectionSorter do + let(:diffs) do + [ + double(new_path: '.dir/test', old_path: '.dir/test'), + double(new_path: '', old_path: '.file'), + double(new_path: '1-folder/A-file.ext', old_path: '1-folder/A-file.ext'), + double(new_path: nil, old_path: '1-folder/M-file.ext'), + double(new_path: '1-folder/Z-file.ext', old_path: '1-folder/Z-file.ext'), + double(new_path: '', old_path: '1-folder/nested/A-file.ext'), + double(new_path: '1-folder/nested/M-file.ext', old_path: '1-folder/nested/M-file.ext'), + double(new_path: nil, old_path: '1-folder/nested/Z-file.ext'), + double(new_path: '2-folder/A-file.ext', old_path: '2-folder/A-file.ext'), + double(new_path: '', old_path: '2-folder/M-file.ext'), + double(new_path: '2-folder/Z-file.ext', old_path: '2-folder/Z-file.ext'), + double(new_path: nil, old_path: '2-folder/nested/A-file.ext'), + double(new_path: 'A-file.ext', old_path: 'A-file.ext'), + double(new_path: '', old_path: 'M-file.ext'), + double(new_path: 'Z-file.ext', old_path: 'Z-file.ext') + ] + end + + subject { described_class.new(diffs) } + + describe '#sort' do + let(:sorted_files_paths) { subject.sort.map { |file| file.new_path.presence || file.old_path } } + + it 'returns list sorted directory first' do + expect(sorted_files_paths).to eq([ + '.dir/test', + '1-folder/nested/A-file.ext', + '1-folder/nested/M-file.ext', + '1-folder/nested/Z-file.ext', + '1-folder/A-file.ext', + '1-folder/M-file.ext', + '1-folder/Z-file.ext', + '2-folder/nested/A-file.ext', + '2-folder/A-file.ext', + '2-folder/M-file.ext', + '2-folder/Z-file.ext', + '.file', + 'A-file.ext', + 'M-file.ext', + 'Z-file.ext' + ]) + end + end +end diff --git a/spec/models/raw_usage_data_spec.rb b/spec/models/raw_usage_data_spec.rb index c10db63da56..7acfb8c19af 100644 --- a/spec/models/raw_usage_data_spec.rb +++ b/spec/models/raw_usage_data_spec.rb @@ -16,28 +16,10 @@ RSpec.describe RawUsageData do describe '#update_sent_at!' do let(:raw_usage_data) { create(:raw_usage_data) } - context 'with save_raw_usage_data feature enabled' do - before do - stub_feature_flags(save_raw_usage_data: true) - end + it 'updates sent_at' do + raw_usage_data.update_sent_at! - it 'updates sent_at' do - raw_usage_data.update_sent_at! - - expect(raw_usage_data.sent_at).not_to be_nil - end - end - - context 'with save_raw_usage_data feature disabled' do - before do - stub_feature_flags(save_raw_usage_data: false) - end - - it 'updates sent_at' do - raw_usage_data.update_sent_at! - - expect(raw_usage_data.sent_at).to be_nil - end + expect(raw_usage_data.sent_at).not_to be_nil end end end diff --git a/spec/serializers/import/bulk_import_entity_spec.rb b/spec/serializers/import/bulk_import_entity_spec.rb index f35684bef20..3dfc659daf7 100644 --- a/spec/serializers/import/bulk_import_entity_spec.rb +++ b/spec/serializers/import/bulk_import_entity_spec.rb @@ -7,14 +7,15 @@ RSpec.describe Import::BulkImportEntity do { 'id' => 1, 'full_name' => 'test', - 'full_path' => 'full/path/test', + 'full_path' => 'full/path/tes', + 'web_url' => 'http://web.url/path', 'foo' => 'bar' } end subject { described_class.represent(importable_data).as_json } - %w[id full_name full_path].each do |attribute| + %w[id full_name full_path web_url].each do |attribute| it "exposes #{attribute}" do expect(subject[attribute.to_sym]).to eq(importable_data[attribute]) end diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb index 2082a163b29..24afa83ef2c 100644 --- a/spec/services/submit_usage_ping_service_spec.rb +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -134,10 +134,9 @@ RSpec.describe SubmitUsagePingService do it_behaves_like 'saves DevOps report data from the response' end - context 'with save_raw_usage_data feature enabled' do + context 'with saving raw_usage_data' do before do stub_response(body: with_dev_ops_score_params) - stub_feature_flags(save_raw_usage_data: true) end it 'creates a raw_usage_data record' do @@ -159,18 +158,6 @@ RSpec.describe SubmitUsagePingService do end end - context 'with save_raw_usage_data feature disabled' do - before do - stub_response(body: with_dev_ops_score_params) - end - - it 'does not create a raw_usage_data record' do - stub_feature_flags(save_raw_usage_data: false) - - expect { subject.execute }.to change(RawUsageData, :count).by(0) - end - end - context 'and usage ping response has unsuccessful status' do before do stub_response(body: nil, status: 504) diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index 469c0c287b1..c9e03ced0dd 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -143,3 +143,55 @@ RSpec.shared_examples 'cacheable diff collection' do end end end + +shared_examples_for 'sortable diff files' do + subject { described_class.new(diffable, **collection_default_args) } + + describe '#raw_diff_files' do + let(:raw_diff_files_paths) do + subject.raw_diff_files(sorted: sorted).map { |file| file.new_path.presence || file.old_path } + end + + context 'when sorted is false (default)' do + let(:sorted) { false } + + it 'returns unsorted diff files' do + expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) + end + end + + context 'when sorted is true' do + let(:sorted) { true } + + it 'returns sorted diff files' do + expect(raw_diff_files_paths).to eq(sorted_diff_files_paths) + end + + context 'when sort_diffs feature flag is disabled' do + before do + stub_feature_flags(sort_diffs: false) + end + + it 'returns unsorted diff files' do + expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) + end + end + end + end +end + +shared_examples_for 'unsortable diff files' do + subject { described_class.new(diffable, **collection_default_args) } + + describe '#raw_diff_files' do + it 'does not call Gitlab::Diff::FileCollectionSorter even when sorted is true' do + # Ensure that diffable is created before expectation to ensure that we are + # not calling it from `FileCollectionSorter` from `#raw_diff_files`. + diffable + + expect(Gitlab::Diff::FileCollectionSorter).not_to receive(:new) + + subject.raw_diff_files(sorted: true) + end + end +end