diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-14 09:09:34 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-14 09:09:34 +0000 |
commit | 97f0ae7454597105a27df65ffb772949d9d4f3cb (patch) | |
tree | 0bf4888e0e9082c8f168a211390a73a6ae810cef | |
parent | 5ebc4d92cd5fbb46c627eb04d500384893dbe2b4 (diff) | |
download | gitlab-ce-97f0ae7454597105a27df65ffb772949d9d4f3cb.tar.gz |
Add latest changes from gitlab-org/gitlab@master
27 files changed, 449 insertions, 114 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 75f7fe62a7e..6301f6a3910 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -188,6 +188,15 @@ const Api = { return axios.get(url, { params }); }, + createProjectMergeRequest(projectPath, options) { + const url = Api.buildUrl(Api.projectMergeRequestsPath).replace( + ':id', + encodeURIComponent(projectPath), + ); + + return axios.post(url, options); + }, + // Return Merge Request for project projectMergeRequest(projectPath, mergeRequestId, params = {}) { const url = Api.buildUrl(Api.projectMergeRequestPath) diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index 3e3dcab70c0..02a0fc7686d 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -3,10 +3,8 @@ import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import { SNIPPET_VISIBILITY_PUBLIC } from '../constants'; import BlobHeader from '~/blob/components/blob_header.vue'; import BlobContent from '~/blob/components/blob_content.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; -import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql'; import GetBlobContent from '../queries/snippet.blob.content.query.graphql'; import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; @@ -16,25 +14,9 @@ export default { BlobEmbeddable, BlobHeader, BlobContent, - GlLoadingIcon, CloneDropdownButton, }, apollo: { - blob: { - query: GetSnippetBlobQuery, - variables() { - return { - ids: this.snippet.id, - }; - }, - update: data => data.snippets.edges[0].node.blob, - result(res) { - const viewer = res.data.snippets.edges[0].node.blob.richViewer - ? RICH_BLOB_VIEWER - : SIMPLE_BLOB_VIEWER; - this.switchViewer(viewer, true); - }, - }, blobContent: { query: GetBlobContent, variables() { @@ -55,18 +37,18 @@ export default { }, data() { return { - blob: {}, + blob: this.snippet.blob, blobContent: '', - activeViewerType: window.location.hash ? SIMPLE_BLOB_VIEWER : '', + activeViewerType: + this.snippet.blob?.richViewer && !window.location.hash + ? RICH_BLOB_VIEWER + : SIMPLE_BLOB_VIEWER, }; }, computed: { embeddable() { return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC; }, - isBlobLoading() { - return this.$apollo.queries.blob.loading; - }, isContentLoading() { return this.$apollo.queries.blobContent.loading; }, @@ -79,8 +61,8 @@ export default { }, }, methods: { - switchViewer(newViewer, respectHash = false) { - this.activeViewerType = respectHash && window.location.hash ? SIMPLE_BLOB_VIEWER : newViewer; + switchViewer(newViewer) { + this.activeViewerType = newViewer; }, }, }; @@ -88,13 +70,7 @@ export default { <template> <div> <blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" /> - <gl-loading-icon - v-if="isBlobLoading" - :label="__('Loading blob')" - size="lg" - class="prepend-top-20 append-bottom-20" - /> - <article v-else class="file-holder snippet-file-content"> + <article class="file-holder snippet-file-content"> <blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer"> <template #actions> <clone-dropdown-button diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql index 22aab7c7795..d793d0b6bb4 100644 --- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql +++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql @@ -1,3 +1,5 @@ +#import '~/graphql_shared/fragments/blobviewer.fragment.graphql' + fragment SnippetBase on Snippet { id title @@ -9,6 +11,19 @@ fragment SnippetBase on Snippet { webUrl httpUrlToRepo sshUrlToRepo + blob { + binary + name + path + rawPath + size + simpleViewer { + ...BlobViewer + } + richViewer { + ...BlobViewer + } + } userPermissions { adminSnippet updateSnippet diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql deleted file mode 100644 index 785c88c185a..00000000000 --- a/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql +++ /dev/null @@ -1,24 +0,0 @@ -#import '~/graphql_shared/fragments/blobviewer.fragment.graphql' - -query SnippetBlobFull($ids: [ID!]) { - snippets(ids: $ids) { - edges { - node { - id - blob { - binary - name - path - rawPath - size - simpleViewer { - ...BlobViewer - } - richViewer { - ...BlobViewer - } - } - } - } - } -} diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js new file mode 100644 index 00000000000..5081d467016 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/constants.js @@ -0,0 +1,12 @@ +import { s__ } from '~/locale'; + +export const BRANCH_SUFFIX_COUNT = 8; +export const DEFAULT_TARGET_BRANCH = 'master'; + +export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.'); +export const SUBMIT_CHANGES_COMMIT_ERROR = s__( + 'StaticSiteEditor|Could not commit the content changes.', +); +export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__( + 'StaticSiteEditor|Could not create merge request.', +); diff --git a/app/assets/javascripts/static_site_editor/services/generate_branch_name.js b/app/assets/javascripts/static_site_editor/services/generate_branch_name.js new file mode 100644 index 00000000000..f45ad616332 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/services/generate_branch_name.js @@ -0,0 +1,8 @@ +import { BRANCH_SUFFIX_COUNT, DEFAULT_TARGET_BRANCH } from '../constants'; + +const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT); + +const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) => + `${username}-${targetBranch}-patch-${generateBranchSuffix()}`; + +export default generateBranchName; diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js index 6b0d8c74ff7..ff591e4b245 100644 --- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js +++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js @@ -1,4 +1,76 @@ -// TODO implement -const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000)); +import Api from '~/api'; +import { s__, sprintf } from '~/locale'; +import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; +import generateBranchName from '~/static_site_editor/services/generate_branch_name'; + +import { + DEFAULT_TARGET_BRANCH, + SUBMIT_CHANGES_BRANCH_ERROR, + SUBMIT_CHANGES_COMMIT_ERROR, + SUBMIT_CHANGES_MERGE_REQUEST_ERROR, +} from '../constants'; + +const createBranch = (projectId, branch) => + Api.createBranch(projectId, { + ref: DEFAULT_TARGET_BRANCH, + branch, + }).catch(() => { + throw new Error(SUBMIT_CHANGES_BRANCH_ERROR); + }); + +const commitContent = (projectId, message, branch, sourcePath, content) => + Api.commitMultiple( + projectId, + convertObjectPropsToSnakeCase({ + branch, + commitMessage: message, + actions: [ + convertObjectPropsToSnakeCase({ + action: 'update', + filePath: sourcePath, + content, + }), + ], + }), + ).catch(() => { + throw new Error(SUBMIT_CHANGES_COMMIT_ERROR); + }); + +const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) => + Api.createProjectMergeRequest( + projectId, + convertObjectPropsToSnakeCase({ + title, + sourceBranch, + targetBranch, + }), + ).catch(() => { + throw new Error(SUBMIT_CHANGES_MERGE_REQUEST_ERROR); + }); + +const submitContentChanges = ({ username, projectId, sourcePath, content }) => { + const branch = generateBranchName(username); + const mergeRequestTitle = sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), { + sourcePath, + }); + const meta = {}; + + return createBranch(projectId, branch) + .then(() => { + Object.assign(meta, { branch: { label: branch } }); + + return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content); + }) + .then(({ data: { short_id: label, web_url: url } }) => { + Object.assign(meta, { commit: { label, url } }); + + return createMergeRequest(projectId, mergeRequestTitle, branch); + }) + .then(({ data: { iid: label, web_url: url } }) => { + Object.assign(meta, { mergeRequest: { label, url } }); + + return meta; + }); +}; export default submitContentChanges; diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js index e457fde591a..477ec540e02 100644 --- a/app/assets/javascripts/static_site_editor/store/state.js +++ b/app/assets/javascripts/static_site_editor/store/state.js @@ -10,6 +10,8 @@ const createState = (initialState = {}) => ({ content: '', title: '', + savedContentMeta: null, + ...initialState, }); diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index cda27890d6b..25121dce005 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -20,8 +20,17 @@ module Resolvers args[:iids] ||= [args[:iid]].compact - args[:iids].map { |iid| batch_load(iid) } - .select(&:itself) # .compact doesn't work on BatchLoader + if args[:iids].any? + batch_load_merge_requests(args[:iids]) + else + args[:project_id] = project.id + + MergeRequestsFinder.new(context[:current_user], args).execute + end + end + + def batch_load_merge_requests(iids) + iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader end # rubocop: disable CodeReuse/ActiveRecord diff --git a/changelogs/unreleased/212561-saving-changes-rest-service.yml b/changelogs/unreleased/212561-saving-changes-rest-service.yml new file mode 100644 index 00000000000..e7d45f4cd92 --- /dev/null +++ b/changelogs/unreleased/212561-saving-changes-rest-service.yml @@ -0,0 +1,5 @@ +--- +title: Save changes in Static Site Editor using REST GitLab API +merge_request: 29286 +author: +type: added diff --git a/changelogs/unreleased/213800-optimize-usage_activity_by_stage-create-protected_branches.yml b/changelogs/unreleased/213800-optimize-usage_activity_by_stage-create-protected_branches.yml new file mode 100644 index 00000000000..ab4233fda75 --- /dev/null +++ b/changelogs/unreleased/213800-optimize-usage_activity_by_stage-create-protected_branches.yml @@ -0,0 +1,5 @@ +--- +title: Optimize protected branches usage data +merge_request: 29148 +author: +type: performance diff --git a/changelogs/unreleased/34527-fix-graphql-endpoint-for-merge-requests.yml b/changelogs/unreleased/34527-fix-graphql-endpoint-for-merge-requests.yml new file mode 100644 index 00000000000..b8cc411e120 --- /dev/null +++ b/changelogs/unreleased/34527-fix-graphql-endpoint-for-merge-requests.yml @@ -0,0 +1,5 @@ +--- +title: Fix pagination in Merge Request GraphQL api +merge_request: 28667 +author: briankabiro +type: fixed diff --git a/config/routes/project.rb b/config/routes/project.rb index 943ff24ffee..77183c963d0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -26,6 +26,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do scope '-' do get 'archive/*id', constraints: { format: Gitlab::PathRegex.archive_formats_regex, id: /.+?/ }, to: 'repositories#archive', as: 'archive' + scope controller: :static_site_editor do + get '/sse/*id', action: :show, as: :show_sse + end + resources :artifacts, only: [:index, :destroy] resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 8a635745907..38d6cdbbaf8 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -67,10 +67,6 @@ scope format: false do end end - scope controller: :static_site_editor do - get '/sse/*id', action: :show, as: :show_sse - end - get '/tree/*id', to: 'tree#show', as: :tree get '/raw/*id', to: 'raw#show', as: :raw get '/blame/*id', to: 'blame#show', as: :blame diff --git a/db/migrate/20200408175424_add_index_on_creator_id_created_at_id_to_projects_table.rb b/db/migrate/20200408175424_add_index_on_creator_id_created_at_id_to_projects_table.rb new file mode 100644 index 00000000000..70df38aea17 --- /dev/null +++ b/db/migrate/20200408175424_add_index_on_creator_id_created_at_id_to_projects_table.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnCreatorIdCreatedAtIdToProjectsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :projects, [:creator_id, :created_at, :id] + end + + def down + remove_concurrent_index :projects, [:creator_id, :created_at, :id] + end +end diff --git a/db/structure.sql b/db/structure.sql index 622234396a2..895afed92e6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9884,6 +9884,8 @@ CREATE INDEX index_projects_on_created_at_and_id ON public.projects USING btree CREATE INDEX index_projects_on_creator_id_and_created_at ON public.projects USING btree (creator_id, created_at); +CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON public.projects USING btree (creator_id, created_at, id); + CREATE INDEX index_projects_on_description_trigram ON public.projects USING gin (description public.gin_trgm_ops); CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON public.projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false)); @@ -13092,5 +13094,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200407094005 20200407094923 20200408110856 +20200408175424 \. diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 61c3d7a4042..d1d0c358dc6 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -468,12 +468,16 @@ config. Manual failover is possible by updating `praefect['virtual_storages']` and nominating a new primary node. - NOTE: **Note:**: Automatic failover is not yet supported for setups with - multiple Praefect nodes. There is currently no coordination between Praefect - nodes, which could result in two Praefect instances thinking two different - Gitaly nodes are the primary. Follow issue - [#2547](https://gitlab.com/gitlab-org/gitaly/-/issues/2547) for - updates. +1. By default, Praefect will nominate a primary Gitaly node for each + shard and store the state of the primary in local memory. This state + does not persist across restarts and will cause a split brain + if multiple Praefect nodes are used for redundancy. + + To avoid this limitation, enable the SQL election strategy: + + ```ruby + praefect['failover_election_strategy'] = 'sql' + ``` 1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure): @@ -677,8 +681,18 @@ current primary node is found to be unhealthy. checks fail for the current primary backend Gitaly node, and new primary will be elected. **Do not use with multiple Praefect nodes!** Using with multiple Praefect nodes is likely to result in a split brain. -- **PostgreSQL:** Coming soon. See isse - [#2547](https://gitlab.com/gitlab-org/gitaly/-/issues/2547) for updates. +- **PostgreSQL:** Enabled by setting + `praefect['failover_election_strategy'] = sql`. This configuration + option will allow multiple Praefect nodes to coordinate via the + PostgreSQL database to elect a primary Gitaly node. This configuration + will cause Praefect nodes to elect a new primary, monitor its health, + and elect a new primary if the current one has not been reachable in + 10 seconds by a majority of the Praefect nodes. + +NOTE: **Note:**: Praefect does not yet account for replication lag on +the secondaries during the election process, so data loss can occur +during a failover. Follow issue +[#2642](https://gitlab.com/gitlab-org/gitaly/-/issues/2642) for updates. It is likely that we will implement support for Consul, and a cloud native strategy in the future. diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb index 9b7a2bd1f3e..de7b4b998be 100644 --- a/lib/api/entities/project_import_status.rb +++ b/lib/api/entities/project_import_status.rb @@ -5,7 +5,7 @@ module API class ProjectImportStatus < ProjectIdentity expose :import_status expose :correlation_id do |project, _options| - project.import_state.correlation_id + project.import_state&.correlation_id end # TODO: Use `expose_nil` once we upgrade the grape-entity gem diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index df48e347511..d95da262eea 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -98,7 +98,6 @@ module Gitlab preview raw refs - sse tree update wikis diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ab06c98a297..2e2289675f4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12189,9 +12189,6 @@ msgstr "" msgid "Loading" msgstr "" -msgid "Loading blob" -msgstr "" - msgid "Loading contribution stats for group members" msgstr "" @@ -19368,6 +19365,15 @@ msgstr "" msgid "Static Application Security Testing (SAST)" msgstr "" +msgid "StaticSiteEditor|Branch could not be created." +msgstr "" + +msgid "StaticSiteEditor|Could not commit the content changes." +msgstr "" + +msgid "StaticSiteEditor|Could not create merge request." +msgstr "" + msgid "StaticSiteEditor|Return to site" msgstr "" @@ -19377,6 +19383,9 @@ msgstr "" msgid "StaticSiteEditor|Summary of changes" msgstr "" +msgid "StaticSiteEditor|Update %{sourcePath} file" +msgstr "" + msgid "StaticSiteEditor|View merge request" msgstr "" diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index fdefa16ac19..f34c2fb69eb 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -651,7 +651,7 @@ describe('Api', () => { describe('when an error occurs while getting a raw file', () => { it('rejects the Promise', () => { - mock.onDelete(expectedUrl).replyOnce(500); + mock.onPost(expectedUrl).replyOnce(500); return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => { expect(mock.history.get).toHaveLength(1); @@ -659,4 +659,36 @@ describe('Api', () => { }); }); }); + + describe('createProjectMergeRequest', () => { + const dummyProjectPath = 'gitlab-org/gitlab'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent( + dummyProjectPath, + )}/merge_requests`; + const options = { + source_branch: 'feature', + target_branch: 'master', + title: 'Add feature', + }; + + describe('when the merge request is successfully created', () => { + it('resolves the Promise', () => { + mock.onPost(expectedUrl, options).replyOnce(201); + + return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => { + expect(mock.history.post).toHaveLength(1); + }); + }); + }); + + describe('when an error occurs while getting a raw file', () => { + it('rejects the Promise', () => { + mock.onPost(expectedUrl).replyOnce(500); + + return Api.createProjectMergeRequest(dummyProjectPath).catch(() => { + expect(mock.history.post).toHaveLength(1); + }); + }); + }); + }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index c4f1dd0ca35..1f6038bc7f0 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -1,5 +1,4 @@ import { mount } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; import BlobHeader from '~/blob/components/blob_header.vue'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; @@ -19,23 +18,15 @@ describe('Blob Embeddable', () => { id: 'gid://foo.bar/snippet', webUrl: 'https://foo.bar', visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + blob: BlobMock, }; const dataMock = { - blob: BlobMock, activeViewerType: SimpleViewerMock.type, }; - function createComponent( - props = {}, - data = dataMock, - blobLoading = false, - contentLoading = false, - ) { + function createComponent(props = {}, data = dataMock, contentLoading = false) { const $apollo = { queries: { - blob: { - loading: blobLoading, - }, blobContent: { loading: contentLoading, }, @@ -87,12 +78,6 @@ describe('Blob Embeddable', () => { expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); }); - it('shows loading icon while blob data is in flight', () => { - createComponent({}, dataMock, true); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find('.snippet-file-content').exists()).toBe(false); - }); - it('sets simple viewer correctly', () => { createComponent(); expect(wrapper.find(SimpleViewer).exists()).toBe(true); @@ -133,14 +118,14 @@ describe('Blob Embeddable', () => { }); it('renders simple viewer by default if URL contains hash', () => { - createComponent(); + createComponent({}, {}); expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); expect(wrapper.find(SimpleViewer).exists()).toBe(true); }); describe('switchViewer()', () => { - it('by default switches to the passed viewer', () => { + it('switches to the passed viewer', () => { createComponent(); wrapper.vm.switchViewer(RichViewerMock.type); @@ -157,22 +142,6 @@ describe('Blob Embeddable', () => { expect(wrapper.find(SimpleViewer).exists()).toBe(true); }); }); - - it('respects hash over richViewer in the blob when corresponding parameter is passed', () => { - createComponent( - {}, - { - blob: BlobMock, - }, - ); - expect(wrapper.vm.blob.richViewer).toEqual(expect.any(Object)); - - wrapper.vm.switchViewer(RichViewerMock.type, true); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); - expect(wrapper.find(SimpleViewer).exists()).toBe(true); - }); - }); }); }); }); diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 9e1c14515e6..1993636ab12 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -34,3 +34,11 @@ export const savedContentMeta = { }; export const submitChangesError = 'Could not save changes'; +export const commitMultipleResponse = { + short_id: 'ed899a2f4b5', + web_url: '/commit/ed899a2f4b5', +}; +export const createMergeRequestResponse = { + iid: '123', + web_url: '/merge_requests/123', +}; diff --git a/spec/frontend/static_site_editor/services/generate_branch_name_spec.js b/spec/frontend/static_site_editor/services/generate_branch_name_spec.js new file mode 100644 index 00000000000..0624fc3b7b4 --- /dev/null +++ b/spec/frontend/static_site_editor/services/generate_branch_name_spec.js @@ -0,0 +1,22 @@ +import { DEFAULT_TARGET_BRANCH, BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants'; +import generateBranchName from '~/static_site_editor/services/generate_branch_name'; + +import { username } from '../mock_data'; + +describe('generateBranchName', () => { + const timestamp = 12345678901234; + + beforeEach(() => { + jest.spyOn(Date, 'now').mockReturnValueOnce(timestamp); + }); + + it('generates a name that includes the username and target branch', () => { + expect(generateBranchName(username)).toMatch(`${username}-${DEFAULT_TARGET_BRANCH}`); + }); + + it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => { + expect(generateBranchName(username)).toMatch( + timestamp.toString().substring(BRANCH_SUFFIX_COUNT), + ); + }); +}); diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js new file mode 100644 index 00000000000..9a0bd88b57d --- /dev/null +++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js @@ -0,0 +1,131 @@ +import Api from '~/api'; +import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; + +import { + DEFAULT_TARGET_BRANCH, + SUBMIT_CHANGES_BRANCH_ERROR, + SUBMIT_CHANGES_COMMIT_ERROR, + SUBMIT_CHANGES_MERGE_REQUEST_ERROR, +} from '~/static_site_editor/constants'; +import generateBranchName from '~/static_site_editor/services/generate_branch_name'; +import submitContentChanges from '~/static_site_editor/services/submit_content_changes'; + +import { + username, + projectId, + commitMultipleResponse, + createMergeRequestResponse, + sourcePath, + sourceContent as content, +} from '../mock_data'; + +jest.mock('~/static_site_editor/services/generate_branch_name'); + +describe('submitContentChanges', () => { + const mergeRequestTitle = `Update ${sourcePath} file`; + const branch = 'branch-name'; + + beforeEach(() => { + jest.spyOn(Api, 'createBranch').mockResolvedValue(); + jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse }); + jest + .spyOn(Api, 'createProjectMergeRequest') + .mockResolvedValue({ data: createMergeRequestResponse }); + + generateBranchName.mockReturnValue(branch); + }); + + it('creates a branch named after the username and target branch', () => { + return submitContentChanges({ username, projectId }).then(() => { + expect(Api.createBranch).toHaveBeenCalledWith(projectId, { + ref: DEFAULT_TARGET_BRANCH, + branch, + }); + }); + }); + + it('notifies error when branch could not be created', () => { + Api.createBranch.mockRejectedValueOnce(); + + expect(submitContentChanges({ username, projectId })).rejects.toThrow( + SUBMIT_CHANGES_BRANCH_ERROR, + ); + }); + + it('commits the content changes to the branch when creating branch succeeds', () => { + return submitContentChanges({ username, projectId, sourcePath, content }).then(() => { + expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, { + branch, + commit_message: mergeRequestTitle, + actions: [ + { + action: 'update', + file_path: sourcePath, + content, + }, + ], + }); + }); + }); + + it('notifies error when content could not be committed', () => { + Api.commitMultiple.mockRejectedValueOnce(); + + expect(submitContentChanges({ username, projectId })).rejects.toThrow( + SUBMIT_CHANGES_COMMIT_ERROR, + ); + }); + + it('creates a merge request when commiting changes succeeds', () => { + return submitContentChanges({ username, projectId, sourcePath, content }).then(() => { + expect(Api.createProjectMergeRequest).toHaveBeenCalledWith( + projectId, + convertObjectPropsToSnakeCase({ + title: mergeRequestTitle, + targetBranch: DEFAULT_TARGET_BRANCH, + sourceBranch: branch, + }), + ); + }); + }); + + it('notifies error when merge request could not be created', () => { + Api.createProjectMergeRequest.mockRejectedValueOnce(); + + expect(submitContentChanges({ username, projectId })).rejects.toThrow( + SUBMIT_CHANGES_MERGE_REQUEST_ERROR, + ); + }); + + describe('when changes are submitted successfully', () => { + let result; + + beforeEach(() => { + return submitContentChanges({ username, projectId, sourcePath, content }).then(_result => { + result = _result; + }); + }); + + it('returns the branch name', () => { + expect(result).toMatchObject({ branch: { label: branch } }); + }); + + it('returns commit short id and web url', () => { + expect(result).toMatchObject({ + commit: { + label: commitMultipleResponse.short_id, + url: commitMultipleResponse.web_url, + }, + }); + }); + + it('returns merge request iid and web url', () => { + expect(result).toMatchObject({ + mergeRequest: { + label: createMergeRequestResponse.iid, + url: createMergeRequestResponse.web_url, + }, + }); + }); + }); +}); diff --git a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb index e260e4463f4..c616310a72c 100644 --- a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb +++ b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe 'getting notes for a merge request' do include GraphqlHelpers - let(:noteable) { create(:merge_request) } + let_it_be(:noteable) { create(:merge_request) } def noteable_query(noteable_fields) <<~QRY diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index e1fe6470881..a1b3111ff71 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -93,4 +93,41 @@ describe 'getting merge request information nested in a project' do expect(merge_request_graphql_data['pipelines']['edges'].size).to eq(1) end end + + context 'when limiting the number of results' do + let(:merge_requests_graphql_data) { graphql_data['project']['mergeRequests']['edges'] } + + let!(:merge_requests) do + [ + create(:merge_request, source_project: project, source_branch: 'branch-1'), + create(:merge_request, source_project: project, source_branch: 'branch-2'), + create(:merge_request, source_project: project, source_branch: 'branch-3') + ] + end + + let(:fields) do + <<~QUERY + edges { + node { + iid, + title + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + "mergeRequests(first: 2) { #{fields} }" + ) + end + + it 'returns the correct number of results' do + post_graphql(query, current_user: current_user) + + expect(merge_requests_graphql_data.size).to eq 2 + end + end end |