summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 09:09:34 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 09:09:34 +0000
commit97f0ae7454597105a27df65ffb772949d9d4f3cb (patch)
tree0bf4888e0e9082c8f168a211390a73a6ae810cef
parent5ebc4d92cd5fbb46c627eb04d500384893dbe2b4 (diff)
downloadgitlab-ce-97f0ae7454597105a27df65ffb772949d9d4f3cb.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue40
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql15
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.query.graphql24
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js12
-rw-r--r--app/assets/javascripts/static_site_editor/services/generate_branch_name.js8
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js76
-rw-r--r--app/assets/javascripts/static_site_editor/store/state.js2
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb13
-rw-r--r--changelogs/unreleased/212561-saving-changes-rest-service.yml5
-rw-r--r--changelogs/unreleased/213800-optimize-usage_activity_by_stage-create-protected_branches.yml5
-rw-r--r--changelogs/unreleased/34527-fix-graphql-endpoint-for-merge-requests.yml5
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/routes/repository.rb4
-rw-r--r--db/migrate/20200408175424_add_index_on_creator_id_created_at_id_to_projects_table.rb17
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/gitaly/praefect.md30
-rw-r--r--lib/api/entities/project_import_status.rb2
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/frontend/api_spec.js34
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js39
-rw-r--r--spec/frontend/static_site_editor/mock_data.js8
-rw-r--r--spec/frontend/static_site_editor/services/generate_branch_name_spec.js22
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js131
-rw-r--r--spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb37
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