summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Knox <psimyn@gmail.com>2017-12-01 14:04:14 +1100
committerSimon Knox <psimyn@gmail.com>2017-12-01 14:04:14 +1100
commitafa6359f1e047fb0b258eaf221a67ea749e2f980 (patch)
treefcf114897077b57e6bed8320f6a4c4a1e458b849
parent44b98938dc059fb231ef0ed23ff29b6bb86de9a1 (diff)
parent3b6fba728fdef4b1824dcfc33539e696e4f17cf5 (diff)
downloadgitlab-ce-afa6359f1e047fb0b258eaf221a67ea749e2f980.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into psimyn-issue-note-refac
-rw-r--r--CHANGELOG.md19
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js73
-rw-r--r--app/assets/javascripts/behaviors/index.js2
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js74
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js454
-rw-r--r--app/assets/javascripts/droplab/constants.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js29
-rw-r--r--app/assets/javascripts/droplab/hook.js2
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue22
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue6
-rw-r--r--app/assets/javascripts/notes/components/issue_note_form.vue12
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/index.js4
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js (renamed from app/assets/javascripts/notes/services/issue_notes_service.js)0
-rw-r--r--app/assets/javascripts/notes/stores/actions.js4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js4
-rw-r--r--app/assets/javascripts/notes/stores/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/project.js3
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue25
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_item.vue36
-rw-r--r--app/assets/javascripts/repo/components/repo.vue32
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue159
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue54
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue5
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue10
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue6
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue10
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue4
-rw-r--r--app/assets/javascripts/repo/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/repo/lib/common/model.js56
-rw-r--r--app/assets/javascripts/repo/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/repo/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/repo/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/repo/lib/editor.js79
-rw-r--r--app/assets/javascripts/repo/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/repo/services/index.js4
-rw-r--r--app/assets/javascripts/repo/stores/getters.js4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/icons.scss42
-rw-r--r--app/assets/stylesheets/framework/selects.scss18
-rw-r--r--app/assets/stylesheets/pages/issues.scss69
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss12
-rw-r--r--app/assets/stylesheets/pages/repo.scss460
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auto_devops_helper.rb2
-rw-r--r--app/helpers/button_helper.rb56
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/clusters/cluster.rb9
-rw-r--r--app/models/clusters/platforms/kubernetes.rb143
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/merge_request.rb37
-rw-r--r--app/models/merge_request_diff.rb72
-rw-r--r--app/models/project.rb14
-rw-r--r--app/models/project_services/kubernetes_service.rb5
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/protected_tag.rb4
-rw-r--r--app/services/base_count_service.rb8
-rw-r--r--app/services/merge_requests/build_service.rb66
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb24
-rw-r--r--app/views/events/_event.atom.builder7
-rw-r--r--app/views/layouts/_flash.html.haml8
-rw-r--r--app/views/layouts/devise.html.haml3
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/forks/_fork_button.html.haml26
-rw-r--r--app/views/projects/forks/new.html.haml17
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml60
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml1
-rw-r--r--changelogs/unreleased/13634-broadcast-message.yml5
-rw-r--r--changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml5
-rw-r--r--changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml5
-rw-r--r--changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml5
-rw-r--r--changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml5
-rw-r--r--changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml5
-rw-r--r--changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml5
-rw-r--r--changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml5
-rw-r--r--changelogs/unreleased/bvl-double-fork.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml5
-rw-r--r--changelogs/unreleased/default-values-for-mr-states.yml5
-rw-r--r--changelogs/unreleased/dm-fix-registry-with-sudo-token.yml5
-rw-r--r--changelogs/unreleased/dm-project-search-performance.yml6
-rw-r--r--changelogs/unreleased/fix-import-uploads-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/issue_40374.yml5
-rw-r--r--changelogs/unreleased/jk-group-mentions-fix.yml5
-rw-r--r--changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml5
-rw-r--r--changelogs/unreleased/protected-branches-names.yml5
-rw-r--r--changelogs/unreleased/use-count_commits-directly.yml5
-rw-r--r--config/webpack.config.js4
-rw-r--r--db/fixtures/test/01_admin.rb15
-rw-r--r--db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb36
-rw-r--r--db/migrate/limits_to_mysql.rb15
-rw-r--r--db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb10
-rw-r--r--db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb17
-rw-r--r--db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb17
-rw-r--r--db/post_migrate/20171124150326_reschedule_fork_network_creation.rb27
-rw-r--r--db/schema.rb6
-rw-r--r--doc/api/notes.md33
-rw-r--r--doc/development/rake_tasks.md2
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/install/installation.md3
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/user/project/integrations/webhooks.md2
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--features/support/capybara.rb36
-rw-r--r--lib/api/helpers/internal_helpers.rb8
-rw-r--r--lib/api/notes.rb7
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb78
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb8
-rw-r--r--lib/gitlab/git/blob.rb1
-rw-r--r--lib/gitlab/git/repository.rb2
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb8
-rw-r--r--lib/gitlab/project_search_results.rb7
-rw-r--r--lib/gitlab/prometheus/queries/query_additional_metrics.rb2
-rw-r--r--package.json5
-rwxr-xr-xscripts/gitaly-test-spawn3
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb40
-rw-r--r--spec/features/auto_deploy_spec.rb88
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb2
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb106
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb248
-rw-r--r--spec/features/logout_spec.rb22
-rw-r--r--spec/features/projects/clusters/interchangeability_spec.rb16
-rw-r--r--spec/features/projects/environments/environment_spec.rb51
-rw-r--r--spec/features/projects/environments/environments_spec.rb33
-rw-r--r--spec/features/projects/fork_spec.rb17
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin688161 -> 343232 bytes
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb4
-rw-r--r--spec/features/projects/tree/create_file_spec.rb4
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb4
-rw-r--r--spec/helpers/button_helper_spec.rb55
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js31
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js76
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js8
-rw-r--r--spec/javascripts/droplab/hook_spec.js2
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js8
-rw-r--r--spec/javascripts/notes/components/issue_discussion_spec.js4
-rw-r--r--spec/javascripts/notes/components/issue_note_app_spec.js6
-rw-r--r--spec/javascripts/notes/components/issue_note_body_spec.js4
-rw-r--r--spec/javascripts/notes/components/issue_note_form_spec.js4
-rw-r--r--spec/javascripts/notes/components/issue_note_spec.js4
-rw-r--r--spec/javascripts/notes/components/note_awards_list_spec.js4
-rw-r--r--spec/javascripts/notes/mock_data.js4
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js8
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js10
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js10
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js12
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js33
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_item_spec.js53
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_spec.js72
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js22
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js10
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js13
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js12
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js5
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js81
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js84
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js120
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js176
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js7
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js128
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js27
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb16
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/project.json735
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb9
-rw-r--r--spec/models/ci/pipeline_spec.rb19
-rw-r--r--spec/models/clusters/cluster_spec.rb1
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb192
-rw-r--r--spec/models/environment_spec.rb51
-rw-r--r--spec/models/merge_request_diff_spec.rb76
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb42
-rw-r--r--spec/requests/api/internal_spec.rb8
-rw-r--r--spec/requests/api/notes_spec.rb124
-rw-r--r--spec/services/base_count_service_spec.rb6
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb24
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/capybara.rb36
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb28
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb2
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb25
-rw-r--r--vendor/assets/javascripts/clipboard.js621
-rw-r--r--vendor/project_templates/express.tar.gzbin5648 -> 5651 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25063 -> 25065 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50838 -> 50845 bytes
-rw-r--r--yarn.lock50
215 files changed, 4694 insertions, 2547 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92dd4d7610f..6088a1b3515 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.2.3 (2017-11-30)
+
+### Fixed (7 changes)
+
+- Fix hashed storage for Import/Export uploads. !15482
+- Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories. !15520
+- Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories. !15600
+- Fix WIP system note not being created.
+- Fix link text from group context.
+- Fix defaults for MR states and merge statuses.
+- Fix pulling and pushing using a personal access token with the sudo scope.
+
+### Performance (3 changes)
+
+- Drastically improve project search performance by no longer searching namespace name.
+- Reuse authors when rendering event Atom feeds.
+- Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside.
+
+
## 10.2.2 (2017-11-23)
### Fixed (5 changes)
diff --git a/Gemfile b/Gemfile
index b6580c28eb7..ad4b1e73fff 100644
--- a/Gemfile
+++ b/Gemfile
@@ -111,7 +111,7 @@ gem 'google-api-client', '~> 0.13.6'
gem 'unf', '~> 0.1.4'
# Seed data
-gem 'seed-fu', '~> 2.3.7'
+gem 'seed-fu', '2.3.6' # Upgrade to > 2.3.7 once https://github.com/mbleigh/seed-fu/issues/123 is solved
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7375fce8b1e..3f4c930c71d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -815,7 +815,7 @@ GEM
rake (>= 0.9, < 13)
sass (~> 3.4.20)
securecompare (1.0.0)
- seed-fu (2.3.7)
+ seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
select2-rails (3.5.9.3)
@@ -1153,7 +1153,7 @@ DEPENDENCIES
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0)
- seed-fu (~> 2.3.7)
+ seed-fu (= 2.3.6)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3)
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
new file mode 100644
index 00000000000..cdea625fc8c
--- /dev/null
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -0,0 +1,73 @@
+import Clipboard from 'clipboard';
+
+function showTooltip(target, title) {
+ const $target = $(target);
+ const originalTitle = $target.data('original-title');
+
+ if (!$target.data('hideTooltip')) {
+ $target
+ .attr('title', title)
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+ }
+}
+
+function genericSuccess(e) {
+ showTooltip(e.trigger, 'Copied');
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ $(e.trigger).blur();
+}
+
+/**
+ * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
+ * See http://clipboardjs.com/#browser-support
+ */
+function genericError(e) {
+ let key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;'; // Command
+ } else {
+ key = 'Ctrl';
+ }
+ showTooltip(e.trigger, `Press ${key}-C to copy`);
+}
+
+export default function initCopyToClipboard() {
+ const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ clipboard.on('error', genericError);
+
+ /**
+ * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
+ * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
+ * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
+ * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
+ * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
+ * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
+ * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
+ * data types to the intended values.
+ */
+ $(document).on('copy', 'body > textarea[readonly]', (e) => {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const text = e.target.value;
+
+ let json;
+ try {
+ json = JSON.parse(text);
+ } catch (ex) {
+ return;
+ }
+
+ if (!json.text || !json.gfm) return;
+
+ e.preventDefault();
+
+ clipboardData.setData('text/plain', json.text);
+ clipboardData.setData('text/x-gfm', json.gfm);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 671532394a9..34e905222b4 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,7 @@
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
+import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
@@ -9,3 +10,4 @@ import './toggler_behavior';
installGlEmojiElement();
initCopyAsGFM();
+initCopyToClipboard();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
deleted file mode 100644
index 1f3c7e1772d..00000000000
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
-
-import Clipboard from 'vendor/clipboard';
-
-var genericError, genericSuccess, showTooltip;
-
-genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied');
- // Clear the selection and blur the trigger so it loses its border
- e.clearSelection();
- return $(e.trigger).blur();
-};
-
-// Safari doesn't support `execCommand`, so instead we inform the user to
-// copy manually.
-//
-// See http://clipboardjs.com/#browser-support
-genericError = function(e) {
- var key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
- }
- return showTooltip(e.trigger, "Press " + key + "-C to copy");
-};
-
-showTooltip = function(target, title) {
- var $target = $(target);
- var originalTitle = $target.data('original-title');
-
- if (!$target.data('hideTooltip')) {
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
- }
-};
-
-$(function() {
- const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
- clipboard.on('success', genericSuccess);
- clipboard.on('error', genericError);
-
- // This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
- // The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
- // attribute that ClipboardJS reads from.
- // When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
- // to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
- // this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
- // `text/plain` and `text/x-gfm` copy data types to the intended values.
- $(document).on('copy', 'body > textarea[readonly]', function(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const text = e.target.value;
-
- let json;
- try {
- json = JSON.parse(text);
- } catch (ex) {
- return;
- }
-
- if (!json.text || !json.gfm) return;
-
- e.preventDefault();
-
- clipboardData.setData('text/plain', json.text);
- clipboardData.setData('text/x-gfm', json.gfm);
- });
-});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index bf40eb3ee11..23425672b16 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -2,6 +2,7 @@
import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
+import { __, sprintf } from './locale';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
@@ -12,28 +13,49 @@ const CREATE_BRANCH = 'create-branch';
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
+ this.availableButton = this.wrapperEl.querySelector('.available');
+ this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
+ this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
- this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
- this.availableButton = this.wrapperEl.querySelector('.available');
+ this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.refInput = this.wrapperEl.querySelector('.js-ref');
+ this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
- this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.branchCreated = false;
+ this.branchIsValid = true;
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.droplabInitialized = false;
+ this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
+ this.isGettingRef = false;
this.mergeRequestCreated = false;
- this.isCreatingBranch = false;
- this.branchCreated = false;
+ this.refDebounce = _.debounce((value, target) => this.getRef(value, target), 500);
+ this.refIsValid = true;
+ this.refsPath = this.wrapperEl.dataset.refsPath;
+ this.suggestedRef = this.refInput.value;
- this.init();
- }
+ // These regexps are used to replace
+ // a backend generated new branch name and its source (ref)
+ // with user's inputs.
+ this.regexps = {
+ branch: {
+ createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
+ createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
+ },
+ ref: {
+ createBranchPath: new RegExp('(ref=)(.+?)$'),
+ createMrPath: new RegExp('(ref=)(.+?)$'),
+ },
+ };
- init() {
- this.checkAbilityToCreateBranch();
+ this.init();
}
available() {
@@ -41,41 +63,13 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.add('hide');
}
- unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
- }
-
- enable() {
- this.createMergeRequestButton.classList.remove('disabled');
- this.createMergeRequestButton.removeAttribute('disabled');
-
- this.dropdownToggle.classList.remove('disabled');
- this.dropdownToggle.removeAttribute('disabled');
- }
-
- disable() {
- this.createMergeRequestButton.classList.add('disabled');
- this.createMergeRequestButton.setAttribute('disabled', 'disabled');
-
- this.dropdownToggle.classList.add('disabled');
- this.dropdownToggle.setAttribute('disabled', 'disabled');
- }
-
- hide() {
- this.wrapperEl.classList.add('hide');
- }
-
- setUnavailableButtonState(isLoading = true) {
- if (isLoading) {
- this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'Checking branch availability…';
- } else {
- this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'New branch unavailable';
- }
+ bindEvents() {
+ this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
+ this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
checkAbilityToCreateBranch() {
@@ -107,49 +101,233 @@ export default class CreateMergeRequestDropdown {
});
}
- initDroplab() {
- this.droplab = new DropLab();
+ createBranch() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createBranchPath,
+ beforeSend: () => (this.isCreatingBranch = true),
+ })
+ .done((data) => {
+ this.branchCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ }
- this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
- this.getDroplabConfig());
+ createMergeRequest() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createMrPath,
+ beforeSend: () => (this.isCreatingMergeRequest = true),
+ })
+ .done((data) => {
+ this.mergeRequestCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ }
+
+ disable() {
+ this.disableCreateAction();
+
+ this.dropdownToggle.classList.add('disabled');
+ this.dropdownToggle.setAttribute('disabled', 'disabled');
+ }
+
+ disableCreateAction() {
+ this.createMergeRequestButton.classList.add('disabled');
+ this.createMergeRequestButton.setAttribute('disabled', 'disabled');
+
+ this.createTargetButton.classList.add('disabled');
+ this.createTargetButton.setAttribute('disabled', 'disabled');
+ }
+
+ enable() {
+ this.createMergeRequestButton.classList.remove('disabled');
+ this.createMergeRequestButton.removeAttribute('disabled');
+
+ this.createTargetButton.classList.remove('disabled');
+ this.createTargetButton.removeAttribute('disabled');
+
+ this.dropdownToggle.classList.remove('disabled');
+ this.dropdownToggle.removeAttribute('disabled');
+ }
+
+ static findByValue(objects, ref, returnFirstMatch = false) {
+ if (!objects || !objects.length) return false;
+ if (objects.indexOf(ref) > -1) return ref;
+ if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item));
+
+ return false;
}
getDroplabConfig() {
return {
- InputSetter: [{
- input: this.createMergeRequestButton,
- valueAttribute: 'data-value',
- inputAttribute: 'data-action',
- }, {
- input: this.createMergeRequestButton,
- valueAttribute: 'data-text',
- }],
+ addActiveClassToDropdownButton: true,
+ InputSetter: [
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-text',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-text',
+ },
+ ],
};
}
- bindEvents() {
- this.createMergeRequestButton
- .addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ static getInputSelectedText(input) {
+ const start = input.selectionStart;
+ const end = input.selectionEnd;
+
+ return input.value.substr(start, end - start);
+ }
+
+ getRef(ref, target = 'all') {
+ if (!ref) return false;
+
+ return $.ajax({
+ method: 'GET',
+ dataType: 'json',
+ url: this.refsPath + ref,
+ beforeSend: () => {
+ this.isGettingRef = true;
+ },
+ })
+ .always(() => {
+ this.isGettingRef = false;
+ })
+ .done((data) => {
+ const branches = data[Object.keys(data)[0]];
+ const tags = data[Object.keys(data)[1]];
+ let result;
+
+ if (target === 'branch') {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref);
+ } else {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
+ CreateMergeRequestDropdown.findByValue(tags, ref, true);
+ this.suggestedRef = result;
+ }
+
+ return this.updateInputState(target, ref, result);
+ })
+ .fail(() => {
+ this.unavailable();
+ this.disable();
+ new Flash('Failed to get ref.');
+
+ return false;
+ });
+ }
+
+ getTargetData(target) {
+ return {
+ input: this[`${target}Input`],
+ message: this[`${target}Message`],
+ };
+ }
+
+ hide() {
+ this.wrapperEl.classList.add('hide');
+ }
+
+ init() {
+ this.checkAbilityToCreateBranch();
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+
+ this.droplab.init(
+ this.dropdownToggle,
+ this.dropdownList,
+ [InputSetter],
+ this.getDroplabConfig(),
+ );
+ }
+
+ inputsAreValid() {
+ return this.branchIsValid && this.refIsValid;
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
- this.branchCreated;
+ this.branchCreated ||
+ this.isGettingRef;
}
- onClickCreateMergeRequestButton(e) {
+ onChangeInput(event) {
+ let target;
+ let value;
+
+ if (event.srcElement === this.branchInput) {
+ target = 'branch';
+ value = this.branchInput.value;
+ } else if (event.srcElement === this.refInput) {
+ target = 'ref';
+ value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
+ event.srcElement.value.slice(event.srcElement.selectionEnd);
+ } else {
+ return false;
+ }
+
+ if (this.isGettingRef) return false;
+
+ // `ENTER` key submits the data.
+ if (event.keyCode === 13 && this.inputsAreValid()) {
+ event.preventDefault();
+ return this.createMergeRequestButton.click();
+ }
+
+ // If the input is empty, use the original value generated by the backend.
+ if (!value) {
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.createMrPath = this.wrapperEl.dataset.createMrPath;
+
+ if (target === 'branch') {
+ this.branchIsValid = true;
+ } else {
+ this.refIsValid = true;
+ }
+
+ this.enable();
+ this.showAvailableMessage(target);
+ return true;
+ }
+
+ this.showCheckingMessage(target);
+ this.refDebounce(value, target);
+
+ return true;
+ }
+
+ onClickCreateMergeRequestButton(event) {
let xhr = null;
- e.preventDefault();
+ event.preventDefault();
if (this.isBusy()) {
return;
}
- if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
+ if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
xhr = this.createMergeRequest();
- } else if (e.target.dataset.action === CREATE_BRANCH) {
+ } else if (event.target.dataset.action === CREATE_BRANCH) {
xhr = this.createBranch();
}
@@ -163,31 +341,131 @@ export default class CreateMergeRequestDropdown {
this.disable();
}
- createMergeRequest() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createMrPath,
- beforeSend: () => (this.isCreatingMergeRequest = true),
- })
- .done((data) => {
- this.mergeRequestCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ onClickSetFocusOnBranchNameInput() {
+ this.branchInput.focus();
}
- createBranch() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createBranchPath,
- beforeSend: () => (this.isCreatingBranch = true),
- })
- .done((data) => {
- this.branchCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ // `TAB` autocompletes the source.
+ static processTab(event) {
+ if (event.keyCode !== 9 || this.isGettingRef) return;
+
+ const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
+
+ // if nothing selected, we don't need to autocomplete anything. Do the default TAB action.
+ // If a user manually selected text, don't autocomplete anything. Do the default TAB action.
+ if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
+
+ event.preventDefault();
+ window.getSelection().removeAllRanges();
+ }
+
+ removeMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
+ const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
+
+ inputClasses.forEach(cssClass => input.classList.remove(cssClass));
+ messageClasses.forEach(cssClass => message.classList.remove(cssClass));
+ message.style.display = 'none';
+ }
+
+ setUnavailableButtonState(isLoading = true) {
+ if (isLoading) {
+ this.unavailableButtonArrow.classList.add('fa-spin');
+ this.unavailableButtonArrow.classList.add('fa-spinner');
+ this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('Checking branch availability...');
+ } else {
+ this.unavailableButtonArrow.classList.remove('fa-spin');
+ this.unavailableButtonArrow.classList.remove('fa-spinner');
+ this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('New branch unavailable');
+ }
+ }
+
+ showAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch name') : __('Source');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-success-outline');
+ message.classList.add('gl-field-success-message');
+ message.textContent = sprintf(__('%{text} is available'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showCheckingMessage(target) {
+ const { message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('branch name') : __('source');
+
+ this.removeMessage(target);
+ message.classList.add('gl-field-hint');
+ message.textContent = sprintf(__('Checking %{text} availability…'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showNotAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-error-outline');
+ message.classList.add('gl-field-error-message');
+ message.textContent = text;
+ message.style.display = 'inline-block';
+ }
+
+ unavailable() {
+ this.availableButton.classList.add('hide');
+ this.unavailableButton.classList.remove('hide');
+ }
+
+ updateInputState(target, ref, result) {
+ // target - 'branch' or 'ref' - which the input field we are searching a ref for.
+ // ref - string - what a user typed.
+ // result - string - what has been found on backend.
+
+ const pathReplacement = `$1${ref}`;
+
+ // If a found branch equals exact the same text a user typed,
+ // that means a new branch cannot be created as it already exists.
+ if (ref === result) {
+ if (target === 'branch') {
+ this.branchIsValid = false;
+ this.showNotAvailableMessage('branch');
+ } else {
+ this.refIsValid = true;
+ this.refInput.dataset.value = ref;
+ this.showAvailableMessage('ref');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
+ pathReplacement);
+ }
+ } else if (target === 'branch') {
+ this.branchIsValid = true;
+ this.showAvailableMessage('branch');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
+ pathReplacement);
+ } else {
+ this.refIsValid = false;
+ this.refInput.dataset.value = ref;
+ this.disableCreateAction();
+ this.showNotAvailableMessage('ref');
+
+ // Show ref hint.
+ if (result) {
+ this.refInput.value = result;
+ this.refInput.setSelectionRange(ref.length, result.length);
+ }
+ }
+
+ if (this.inputsAreValid()) {
+ this.enable();
+ } else {
+ this.disableCreateAction();
+ }
}
}
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index 868d47e91b3..673e9bb4c0f 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
+const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
+ IGNORE_HIDING_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 3901bb177fe..5eb0a339a1c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,15 +1,18 @@
import utils from './utils';
-import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
+import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown {
- constructor(list) {
+ constructor(list, config = {}) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
-
this.eventWrapper = {};
+ if (config.addActiveClassToDropdownButton) {
+ this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
+ }
+
this.getItems();
this.initTemplateString();
this.addEvents();
@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected);
e.preventDefault();
- this.hide();
+ if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', {
detail: {
@@ -67,7 +70,20 @@ class DropDown {
addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
+ this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
+
this.list.addEventListener('click', this.eventWrapper.clickEvent);
+ this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
+ }
+
+ closeDropdown(event) {
+ // `ESC` key closes the dropdown.
+ if (event.keyCode === 27) {
+ event.preventDefault();
+ return this.toggle();
+ }
+
+ return true;
}
setData(data) {
@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
}
hide() {
@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
}
toggle() {
@@ -128,6 +148,7 @@ class DropDown {
destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
+ this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
}
static setImagesSrc(template) {
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/droplab/hook.js
index cf78165b0d8..8a8dcde9f88 100644
--- a/app/assets/javascripts/droplab/hook.js
+++ b/app/assets/javascripts/droplab/hook.js
@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook {
constructor(trigger, list, plugins, config) {
this.trigger = trigger;
- this.list = new DropDown(list);
+ this.list = new DropDown(list, config);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5e0edd823be..dcc0fa63b63 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -44,7 +44,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
-import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index 4bf86611e53..78986a450c2 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -22,7 +22,7 @@
noteType: constants.COMMENT,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
- issueState: this.$store.getters.getIssueData.state,
+ issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
@@ -46,7 +46,7 @@
...mapGetters([
'getCurrentUserLastNote',
'getUserData',
- 'getIssueData',
+ 'getNoteableData',
'getNotesData',
]),
isLoggedIn() {
@@ -59,7 +59,7 @@
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
canCreateNote() {
- return this.getIssueData.current_user.can_create_note;
+ return this.getNoteableData.current_user.can_create_note;
},
issueActionButtonTitle() {
if (this.note.length) {
@@ -85,16 +85,16 @@
return this.getNotesData.quickActionsDocsPath;
},
markdownPreviewPath() {
- return this.getIssueData.preview_note_path;
+ return this.getNoteableData.preview_note_path;
},
author() {
return this.getUserData;
},
canUpdateIssue() {
- return this.getIssueData.current_user.can_update;
+ return this.getNoteableData.current_user.can_update;
},
endpoint() {
- return this.getIssueData.create_note_path;
+ return this.getNoteableData.create_note_path;
},
},
methods: {
@@ -119,7 +119,7 @@
data: {
note: {
noteable_type: constants.NOTEABLE_TYPE,
- noteable_id: this.getIssueData.id,
+ noteable_id: this.getNoteableData.id,
note: this.note,
},
},
@@ -207,7 +207,7 @@
},
initAutoSave() {
if (this.isLoggedIn) {
- this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
+ this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
}
},
initTaskList() {
@@ -269,9 +269,9 @@
<div class="error-alert"></div>
<issue-warning
- v-if="hasWarning(getIssueData)"
- :is-locked="isLocked(getIssueData)"
- :is-confidential="isConfidential(getIssueData)"
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index cb622a572fc..460fde9b62a 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -39,7 +39,7 @@
],
computed: {
...mapGetters([
- 'getIssueData',
+ 'getNoteableData',
]),
discussion() {
return this.note.notes[0];
@@ -48,10 +48,10 @@
return this.discussion.author;
},
canReply() {
- return this.getIssueData.current_user.can_create_note;
+ return this.getNoteableData.current_user.can_create_note;
},
newNotePath() {
- return this.getIssueData.create_note_path;
+ return this.getNoteableData.create_note_path;
},
lastUpdatedBy() {
const { notes } = this.note;
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue
index e2539d6b89d..4d527cb6643 100644
--- a/app/assets/javascripts/notes/components/issue_note_form.vue
+++ b/app/assets/javascripts/notes/components/issue_note_form.vue
@@ -46,8 +46,8 @@
computed: {
...mapGetters([
'getDiscussionLastNote',
- 'getIssueData',
- 'getIssueDataByProp',
+ 'getNoteableData',
+ 'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
@@ -55,7 +55,7 @@
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
- return this.getIssueDataByProp('preview_note_path');
+ return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
@@ -129,9 +129,9 @@
class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning
- v-if="hasWarning(getIssueData)"
- :is-locked="isLocked(getIssueData)"
- :is-confidential="isConfidential(getIssueData)"
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
/>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
index 5c9119644e3..4cfcffa2391 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -14,7 +14,7 @@
export default {
name: 'issueNotesApp',
props: {
- issueData: {
+ noteableData: {
type: Object,
required: true,
},
@@ -56,7 +56,7 @@
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData',
- setIssueData: 'setIssueData',
+ setNoteableData: 'setNoteableData',
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
@@ -106,7 +106,7 @@
},
created() {
this.setNotesData(this.notesData);
- this.setIssueData(this.issueData);
+ this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
},
mounted() {
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e2ea37408cf..8d74c5de5cf 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const notesDataset = document.getElementById('js-vue-notes').dataset;
return {
- issueData: JSON.parse(notesDataset.issueData),
+ noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: {
lastFetchedAt: notesDataset.lastFetchedAt,
@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) {
return createElement('issue-notes-app', {
props: {
- issueData: this.issueData,
+ noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
},
diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index b51b0cb2013..b51b0cb2013 100644
--- a/app/assets/javascripts/notes/services/issue_notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 6f04aecc9b7..085b18642ba 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -4,7 +4,7 @@ import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
import * as constants from '../constants';
-import service from '../services/issue_notes_service';
+import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
-export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data);
+export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 1f0c6af6156..e18b277119e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
export const getNotesDataByProp = state => prop => state.notesData[prop];
-export const getIssueData = state => state.issueData;
-export const getIssueDataByProp = state => prop => state.issueData[prop];
+export const getNoteableData = state => state.noteableData;
+export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 8e0c8531bbc..488a9ca38d3 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -15,7 +15,7 @@ export default new Vuex.Store({
// holds endpoints and permissions provided through haml
notesData: {},
userData: {},
- issueData: {},
+ noteableData: {},
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index cd71533ba9d..d520c197407 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
-export const SET_ISSUE_DATA = 'SET_ISSUE_DATA';
+export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c2a08f3d6fe..20f81a430c2 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -66,8 +66,8 @@ export default {
Object.assign(state, { notesData: data });
},
- [types.SET_ISSUE_DATA](state, data) {
- Object.assign(state, { issueData: data });
+ [types.SET_NOTEABLE_DATA](state, data) {
+ Object.assign(state, { noteableData: data });
},
[types.SET_USER_DATA](state, data) {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 36b6a5ed376..3131e71d9d6 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -17,13 +17,14 @@ export default class Project {
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
+ const activeText = $this.find('.dropdown-menu-inner-title').text();
e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
- $cloneBtnText.text($this.text());
+ $cloneBtnText.text(activeText);
return $('.clone').text(url);
});
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
index fe5179de206..d482a7025de 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
@@ -48,6 +48,27 @@ export default {
}
return this.projectName;
},
+ /**
+ * Smartly truncates project namespace by doing two things;
+ * 1. Only include Group names in path by removing project name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of project name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing project namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ const namespaceArr = this.namespace.split(' / ');
+ namespaceArr.splice(-1, 1);
+ let namespace = namespaceArr.join(' / ');
+
+ if (namespaceArr.length > 2) {
+ namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
+ }
+
+ return namespace;
+ },
},
};
</script>
@@ -87,9 +108,7 @@ export default {
<div
class="project-namespace"
:title="namespace"
- >
- {{namespace}}
- </div>
+ >{{truncatedNamespace}}</div>
</div>
</a>
</li>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..fb862e7bf01
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
@@ -0,0 +1,89 @@
+<script>
+ import icon from '../../../vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+ >
+ <icon
+ name="list-bulleted"
+ :size="18"
+ css-classes="append-right-default"
+ />
+ <template v-if="!collapsed">
+ {{ title }}
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-right"
+ >
+ </i>
+ </button>
+ </template>
+ </header>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="collapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ <div
+ v-else
+ class="help-block prepend-top-0"
+ >
+ No changes
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
new file mode 100644
index 00000000000..6a0262f271b
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
@@ -0,0 +1,35 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ computed: {
+ ...mapGetters([
+ 'addedFiles',
+ 'modifiedFiles',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-list-collapsed text-center"
+ >
+ <icon
+ name="file-addition"
+ :size="18"
+ css-classes="multi-file-addition append-bottom-10"
+ />
+ {{ addedFiles.length }}
+ <icon
+ name="file-modified"
+ :size="18"
+ css-classes="multi-file-modified prepend-top-10 append-bottom-10"
+ />
+ {{ modifiedFiles.length }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
new file mode 100644
index 00000000000..742f746e02f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
@@ -0,0 +1,36 @@
+<script>
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
+ },
+ iconClass() {
+ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list-item">
+ <icon
+ :name="iconName"
+ :size="16"
+ :css-classes="iconClass"
+ />
+ <span class="multi-file-commit-list-path">
+ {{ file.path }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 98117802016..a00e1e9d809 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -40,20 +40,24 @@ export default {
</script>
<template>
- <div class="repository-view">
- <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
- <repo-sidebar/>
- <div
- v-if="isCollapsed"
- class="panel-right"
- >
- <repo-tabs/>
- <component
- :is="currentBlobView"
- />
- <repo-file-buttons/>
- </div>
+ <div
+ class="multi-file"
+ :class="{
+ 'is-collapsed': isCollapsed
+ }"
+ >
+ <repo-sidebar/>
+ <div
+ v-if="isCollapsed"
+ class="multi-file-edit-pane"
+ >
+ <repo-tabs />
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons />
</div>
- <repo-commit-section v-if="changedFiles.length" />
+ <repo-commit-section />
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 377e3d65348..d3344d0c8dc 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,11 +1,18 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
+import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '../../vue_shared/components/icon.vue';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import { n__ } from '../../locale';
+import commitFilesList from './commit_sidebar/list.vue';
export default {
components: {
PopupDialog,
+ icon,
+ commitFilesList,
+ },
+ directives: {
+ tooltip,
},
data() {
return {
@@ -13,6 +20,7 @@ export default {
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
+ collapsed: true,
};
},
computed: {
@@ -23,10 +31,10 @@ export default {
'changedFiles',
]),
commitButtonDisabled() {
- return !this.commitMessage || this.submitCommitsLoading;
+ return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
},
- commitButtonText() {
- return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
+ commitMessageCount() {
+ return this.commitMessage.length;
},
},
methods: {
@@ -77,12 +85,20 @@ export default {
this.submitCommitsLoading = false;
});
},
+ toggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ },
},
};
</script>
<template>
-<div id="commit-area">
+<div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+>
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
@@ -92,78 +108,71 @@ export default {
@toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/>
+ <button
+ v-if="collapsed"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-left"
+ >
+ </i>
+ </button>
+ <commit-files-list
+ title="Staged"
+ :file-list="changedFiles"
+ :collapsed="collapsed"
+ @toggleCollapsed="toggleCollapsed"
+ />
<form
- class="form-horizontal"
- @submit.prevent="tryCommit()">
- <fieldset>
- <div class="form-group">
- <label class="col-md-4 control-label staged-files">
- Staged files ({{changedFiles.length}})
- </label>
- <div class="col-md-6">
- <ul class="list-unstyled changed-files">
- <li
- v-for="(file, index) in changedFiles"
- :key="index">
- <span class="help-block">
- {{ file.path }}
- </span>
- </li>
- </ul>
- </div>
- </div>
- <div class="form-group">
- <label
- class="col-md-4 control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-md-6">
- <textarea
- id="commit-message"
- class="form-control"
- name="commit-message"
- v-model="commitMessage">
- </textarea>
- </div>
- </div>
- <div class="form-group target-branch">
- <label
- class="col-md-4 control-label"
- for="target-branch">
- Target branch
- </label>
- <div class="col-md-6">
- <span class="help-block">
- {{currentBranch}}
- </span>
- </div>
- </div>
- <div class="col-md-offset-4 col-md-6">
- <button
- type="submit"
- :disabled="commitButtonDisabled"
- class="btn btn-success">
- <i
- v-if="submitCommitsLoading"
- class="js-commit-loading-icon fa fa-spinner fa-spin"
- aria-hidden="true"
- aria-label="loading">
- </i>
- <span class="commit-summary">
- {{ commitButtonText }}
- </span>
- </button>
- </div>
- <div class="col-md-offset-4 col-md-6">
- <div class="checkbox">
- <label>
- <input type="checkbox" v-model="startNewMR">
- <span>Start a <strong>new merge request</strong> with these changes</span>
- </label>
- </div>
+ class="form-horizontal multi-file-commit-form"
+ @submit.prevent="tryCommit"
+ v-if="!collapsed"
+ >
+ <div class="multi-file-commit-fieldset">
+ <textarea
+ class="form-control multi-file-commit-message"
+ name="commit-message"
+ v-model="commitMessage"
+ placeholder="Commit message"
+ >
+ </textarea>
+ </div>
+ <div class="multi-file-commit-fieldset">
+ <label
+ v-tooltip
+ title="Create a new merge request with these changes"
+ data-container="body"
+ data-placement="top"
+ >
+ <input
+ type="checkbox"
+ v-model="startNewMR"
+ />
+ Merge Request
+ </label>
+ <button
+ type="submit"
+ :disabled="commitButtonDisabled"
+ class="btn btn-default btn-sm append-right-10 prepend-left-10"
+ >
+ <i
+ v-if="submitCommitsLoading"
+ class="js-commit-loading-icon fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="loading"
+ >
+ </i>
+ Commit
+ </button>
+ <div
+ class="multi-file-commit-message-count"
+ >
+ {{ commitMessageCount }}
</div>
- </fieldset>
+ </div>
</form>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 1c864b176b1..f37cbd1e961 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -3,19 +3,18 @@
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
+import Editor from '../lib/editor';
export default {
- destroyed() {
- if (this.monacoInstance) {
- this.monacoInstance.destroy();
- }
+ beforeDestroy() {
+ this.editor.dispose();
},
mounted() {
- if (this.monaco) {
+ if (this.editor && monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
- this.monaco = monaco;
+ this.editor = Editor.create(monaco);
this.initMonaco();
});
@@ -29,47 +28,25 @@ export default {
initMonaco() {
if (this.shouldHideEditor) return;
- if (this.monacoInstance) {
- this.monacoInstance.setModel(null);
- }
+ this.editor.clearEditor();
this.getRawFileData(this.activeFile)
.then(() => {
- if (!this.monacoInstance) {
- this.monacoInstance = this.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- });
-
- this.languages = this.monaco.languages.getLanguages();
-
- this.addMonacoEvents();
- }
-
- this.setupEditor();
+ this.editor.createInstance(this.$refs.editor);
})
+ .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
if (!this.activeFile) return;
- const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
- const foundLang = this.languages.find(lang =>
- lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
- );
- const newModel = this.monaco.editor.createModel(
- content, foundLang ? foundLang.id : 'plaintext',
- );
+ const model = this.editor.createModel(this.activeFile);
- this.monacoInstance.setModel(newModel);
- },
- addMonacoEvents() {
- this.monacoInstance.onKeyUp(() => {
+ this.editor.attachModel(model);
+ model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
- content: this.monacoInstance.getValue(),
+ content: m.getValue(),
});
});
},
@@ -99,9 +76,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-if="shouldHideEditor"
+ v-show="shouldHideEditor"
v-html="activeFile.html"
>
</div>
+ <div
+ v-show="!shouldHideEditor"
+ ref="editor"
+ >
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 5be47d568e7..75787ad6103 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -55,7 +55,7 @@
class="file"
@click.prevent="clickedTreeRow(file)">
<td
- class="multi-file-table-col-name"
+ class="multi-file-table-name"
:colspan="submoduleColSpan"
>
<i
@@ -85,12 +85,11 @@
</td>
<template v-if="!isCollapsed && !isSubmodule">
- <td class="hidden-sm hidden-xs">
+ <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
- class="commit-message"
>
{{ file.lastCommit.message }}
</a>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index dd948ee84fb..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -22,12 +22,12 @@ export default {
<template>
<div
v-if="showButtons"
- class="repo-file-buttons"
+ class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
target="_blank"
- class="btn btn-default raw"
+ class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
{{ rawDownloadButtonLabel }}
</a>
@@ -38,17 +38,17 @@ export default {
aria-label="File actions">
<a
:href="activeFile.blamePath"
- class="btn btn-default blame">
+ class="btn btn-default btn-sm blame">
Blame
</a>
<a
:href="activeFile.commitsPath"
- class="btn btn-default history">
+ class="btn btn-default btn-sm history">
History
</a>
<a
:href="activeFile.permalink"
- class="btn btn-default permalink">
+ class="btn btn-default btn-sm permalink">
Permalink
</a>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index d1883299bd9..6ce9267f598 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -32,10 +32,12 @@ export default {
</script>
<template>
-<div class="blob-viewer-container">
+<div>
<div
v-if="!activeFile.renderError"
- v-html="activeFile.html">
+ v-html="activeFile.html"
+ class="multi-file-preview-holder"
+ >
</div>
<div
v-else-if="activeFile.tempFile"
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 9365b09326f..4ea21913129 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -44,20 +44,16 @@ export default {
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
+<div class="ide-file-list">
<table class="table">
<thead>
<tr>
<th
v-if="isCollapsed"
- class="repo-file-options title"
>
- <strong class="clgray">
- {{ projectName }}
- </strong>
</th>
<template v-else>
- <th class="name multi-file-table-col-name">
+ <th class="name multi-file-table-name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
@@ -79,7 +75,7 @@ export default {
:key="n"
/>
<repo-file
- v-for="(file, index) in treeList"
+ v-for="file in treeList"
:key="file.key"
:file="file"
/>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index da0714c368c..fb29a60df66 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -36,27 +36,32 @@ export default {
<template>
<li
- :class="{ active : tab.active }"
@click="setFileActive(tab)"
>
<button
type="button"
- class="close-btn"
+ class="multi-file-tab-close"
@click.stop.prevent="closeFile({ file: tab })"
- :aria-label="closeLabel">
+ :aria-label="closeLabel"
+ :class="{
+ 'modified': tab.changed,
+ }"
+ :disabled="tab.changed"
+ >
<i
class="fa"
:class="changedClass"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</button>
- <a
- href="#"
- class="repo-tab"
+ <div
+ class="multi-file-tab"
+ :class="{active : tab.active }"
:title="tab.url"
- @click.prevent.stop="setFileActive(tab)">
- {{tab.name}}
- </a>
+ >
+ {{ tab.name }}
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index 59beae53e8d..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -16,14 +16,12 @@
<template>
<ul
- id="tabs"
- class="list-unstyled"
+ class="multi-file-tabs list-unstyled append-bottom-0"
>
<repo-tab
v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
- <li class="tabs-divider" />
</ul>
</template>
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/repo/lib/common/disposable.js
new file mode 100644
index 00000000000..84b29bdb600
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/disposable.js
@@ -0,0 +1,14 @@
+export default class Disposable {
+ constructor() {
+ this.disposers = new Set();
+ }
+
+ add(...disposers) {
+ disposers.forEach(disposer => this.disposers.add(disposer));
+ }
+
+ dispose() {
+ this.disposers.forEach(disposer => disposer.dispose());
+ this.disposers.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js
new file mode 100644
index 00000000000..23c4811e6c0
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model.js
@@ -0,0 +1,56 @@
+/* global monaco */
+import Disposable from './disposable';
+
+export default class Model {
+ constructor(monaco, file) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.file = file;
+ this.content = file.content !== '' ? file.content : file.raw;
+
+ this.disposable.add(
+ this.originalModel = this.monaco.editor.createModel(
+ this.file.raw,
+ undefined,
+ new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ ),
+ this.model = this.monaco.editor.createModel(
+ this.content,
+ undefined,
+ new this.monaco.Uri(null, null, this.file.path),
+ ),
+ );
+
+ this.events = new Map();
+ }
+
+ get url() {
+ return this.model.uri.toString();
+ }
+
+ get path() {
+ return this.file.path;
+ }
+
+ getModel() {
+ return this.model;
+ }
+
+ getOriginalModel() {
+ return this.originalModel;
+ }
+
+ onChange(cb) {
+ this.events.set(
+ this.path,
+ this.disposable.add(
+ this.model.onDidChangeContent(e => cb(this.model, e)),
+ ),
+ );
+ }
+
+ dispose() {
+ this.disposable.dispose();
+ this.events.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/repo/lib/common/model_manager.js
new file mode 100644
index 00000000000..fd462252795
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model_manager.js
@@ -0,0 +1,32 @@
+import Disposable from './disposable';
+import Model from './model';
+
+export default class ModelManager {
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.models = new Map();
+ }
+
+ hasCachedModel(path) {
+ return this.models.has(path);
+ }
+
+ addModel(file) {
+ if (this.hasCachedModel(file.path)) {
+ return this.models.get(file.path);
+ }
+
+ const model = new Model(this.monaco, file);
+ this.models.set(model.path, model);
+ this.disposable.add(model);
+
+ return model;
+ }
+
+ dispose() {
+ // dispose of all the models
+ this.disposable.dispose();
+ this.models.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/repo/lib/decorations/controller.js
new file mode 100644
index 00000000000..0954b7973c4
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/decorations/controller.js
@@ -0,0 +1,43 @@
+export default class DecorationsController {
+ constructor(editor) {
+ this.editor = editor;
+ this.decorations = new Map();
+ this.editorDecorations = new Map();
+ }
+
+ getAllDecorationsForModel(model) {
+ if (!this.decorations.has(model.url)) return [];
+
+ const modelDecorations = this.decorations.get(model.url);
+ const decorations = [];
+
+ modelDecorations.forEach(val => decorations.push(...val));
+
+ return decorations;
+ }
+
+ addDecorations(model, decorationsKey, decorations) {
+ const decorationMap = this.decorations.get(model.url) || new Map();
+
+ decorationMap.set(decorationsKey, decorations);
+
+ this.decorations.set(model.url, decorationMap);
+
+ this.decorate(model);
+ }
+
+ decorate(model) {
+ const decorations = this.getAllDecorationsForModel(model);
+ const oldDecorations = this.editorDecorations.get(model.url) || [];
+
+ this.editorDecorations.set(
+ model.url,
+ this.editor.instance.deltaDecorations(oldDecorations, decorations),
+ );
+ }
+
+ dispose() {
+ this.decorations.clear();
+ this.editorDecorations.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js
new file mode 100644
index 00000000000..dc0b1c95e59
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/controller.js
@@ -0,0 +1,71 @@
+/* global monaco */
+import { throttle } from 'underscore';
+import DirtyDiffWorker from './diff_worker';
+import Disposable from '../common/disposable';
+
+export const getDiffChangeType = (change) => {
+ if (change.modified) {
+ return 'modified';
+ } else if (change.added) {
+ return 'added';
+ } else if (change.removed) {
+ return 'removed';
+ }
+
+ return '';
+};
+
+export const getDecorator = change => ({
+ range: new monaco.Range(
+ change.lineNumber,
+ 1,
+ change.endLineNumber,
+ 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
+ },
+});
+
+export default class DirtyDiffController {
+ constructor(modelManager, decorationsController) {
+ this.disposable = new Disposable();
+ this.editorSimpleWorker = null;
+ this.modelManager = modelManager;
+ this.decorationsController = decorationsController;
+ this.dirtyDiffWorker = new DirtyDiffWorker();
+ this.throttledComputeDiff = throttle(this.computeDiff, 250);
+ this.decorate = this.decorate.bind(this);
+
+ this.dirtyDiffWorker.addEventListener('message', this.decorate);
+ }
+
+ attachModel(model) {
+ model.onChange(() => this.throttledComputeDiff(model));
+ }
+
+ computeDiff(model) {
+ this.dirtyDiffWorker.postMessage({
+ path: model.path,
+ originalContent: model.getOriginalModel().getValue(),
+ newContent: model.getModel().getValue(),
+ });
+ }
+
+ reDecorate(model) {
+ this.decorationsController.decorate(model);
+ }
+
+ decorate({ data }) {
+ const decorations = data.changes.map(change => getDecorator(change));
+ this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ this.dirtyDiffWorker.removeEventListener('message', this.decorate);
+ this.dirtyDiffWorker.terminate();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/repo/lib/diff/diff.js
new file mode 100644
index 00000000000..0e37f5c4704
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff.js
@@ -0,0 +1,30 @@
+import { diffLines } from 'diff';
+
+// eslint-disable-next-line import/prefer-default-export
+export const computeDiff = (originalContent, newContent) => {
+ const changes = diffLines(originalContent, newContent);
+
+ let lineNumber = 1;
+ return changes.reduce((acc, change) => {
+ const findOnLine = acc.find(c => c.lineNumber === lineNumber);
+
+ if (findOnLine) {
+ Object.assign(findOnLine, change, {
+ modified: true,
+ endLineNumber: (lineNumber + change.count) - 1,
+ });
+ } else if ('added' in change || 'removed' in change) {
+ acc.push(Object.assign({}, change, {
+ lineNumber,
+ modified: undefined,
+ endLineNumber: (lineNumber + change.count) - 1,
+ }));
+ }
+
+ if (!change.removed) {
+ lineNumber += change.count;
+ }
+
+ return acc;
+ }, []);
+};
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/repo/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..e74c4046330
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff_worker.js
@@ -0,0 +1,10 @@
+import { computeDiff } from './diff';
+
+self.addEventListener('message', (e) => {
+ const data = e.data;
+
+ self.postMessage({
+ path: data.path,
+ changes: computeDiff(data.originalContent, data.newContent),
+ });
+});
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js
new file mode 100644
index 00000000000..db499444402
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor.js
@@ -0,0 +1,79 @@
+import DecorationsController from './decorations/controller';
+import DirtyDiffController from './diff/controller';
+import Disposable from './common/disposable';
+import ModelManager from './common/model_manager';
+import editorOptions from './editor_options';
+
+export default class Editor {
+ static create(monaco) {
+ this.editorInstance = new Editor(monaco);
+
+ return this.editorInstance;
+ }
+
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.currentModel = null;
+ this.instance = null;
+ this.dirtyDiffController = null;
+ this.disposable = new Disposable();
+
+ this.disposable.add(
+ this.modelManager = new ModelManager(this.monaco),
+ this.decorationsController = new DecorationsController(this),
+ );
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ this.disposable.add(
+ this.instance = this.monaco.editor.create(domElement, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ }),
+ this.dirtyDiffController = new DirtyDiffController(
+ this.modelManager, this.decorationsController,
+ ),
+ );
+ }
+ }
+
+ createModel(file) {
+ return this.modelManager.addModel(file);
+ }
+
+ attachModel(model) {
+ this.instance.setModel(model.getModel());
+ this.dirtyDiffController.attachModel(model);
+
+ this.currentModel = model;
+
+ this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
+ Object.keys(obj).forEach((key) => {
+ Object.assign(acc, {
+ [key]: obj[key](model),
+ });
+ });
+ return acc;
+ }, {}));
+
+ this.dirtyDiffController.reDecorate(model);
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ // dispose main monaco instance
+ if (this.instance) {
+ this.instance = null;
+ }
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/repo/lib/editor_options.js
new file mode 100644
index 00000000000..701affc466e
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor_options.js
@@ -0,0 +1,2 @@
+export default [{
+}];
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
index 2fb45dcb03c..994d325e991 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/repo/services/index.js
@@ -16,6 +16,10 @@ export default {
return Promise.resolve(file.content);
}
+ if (file.raw) {
+ return Promise.resolve(file.raw);
+ }
+
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
index 1ed05ac6e35..5ce9f449905 100644
--- a/app/assets/javascripts/repo/stores/getters.js
+++ b/app/assets/javascripts/repo/stores/getters.js
@@ -34,3 +34,7 @@ export const canEditFile = (state) => {
openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
+
+export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
+
+export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 579bd48fac6..30d5d7a653b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
}
&:hover {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 9e45ed52163..e2084e8f85f 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,62 +1,48 @@
.ci-status-icon-success,
.ci-status-icon-passed {
- &,
- &:hover,
- &:focus {
- color: $green-500;
+ svg {
+ fill: $green-500;
}
}
.ci-status-icon-failed {
- &,
- &:hover,
- &:focus {
- color: $gl-danger;
+ svg {
+ fill: $gl-danger;
}
}
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
- &,
- &:hover,
- &:focus {
- color: $orange-500;
+ svg {
+ fill: $orange-500;
}
}
.ci-status-icon-running {
- &,
- &:hover,
- &:focus {
- color: $blue-400;
+ svg {
+ fill: $blue-400;
}
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
- &,
- &:hover,
- &:focus {
- color: $gl-text-color;
+ svg {
+ fill: $gl-text-color;
}
}
.ci-status-icon-created,
.ci-status-icon-skipped {
- &,
- &:hover,
- &:focus {
- color: $gray-darkest;
+ svg {
+ fill: $gray-darkest;
}
}
.ci-status-icon-manual {
- &,
- &:hover,
- &:focus {
- color: $gl-text-color;
+ svg {
+ fill: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index bb70b270299..dbee7073975 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -134,19 +134,22 @@
}
.select2-search {
- padding: 15px 15px 5px;
+ padding: $grid-size;
.select2-drop-auto-width & {
- padding: 15px 15px 5px;
+ padding: $grid-size;
}
input {
- padding: 2px 25px 2px 5px;
+ padding: $grid-size;
background: $white-light image-url('select2.png');
+ background-clip: content-box;
+ background-origin: content-box;
background-repeat: no-repeat;
- background-position: right 0 bottom 6px;
+ background-position: right 0 bottom 0 !important;
border: 1px solid $input-border;
border-radius: $border-radius-default;
+ line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
@@ -156,11 +159,16 @@
&.select2-active {
background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
+ background-origin: content-box;
background-repeat: no-repeat;
- background-position: right 5px center !important;
+ background-position: right 6px center !important;
background-size: 16px 16px !important;
}
}
+
+ + .select2-results {
+ padding-top: 0;
+ }
}
.select2-results {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8bb68ad2425..d3dda2e7d25 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -203,7 +203,24 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
- @include new-style-dropdown;
+ .branch-message,
+ .ref-message {
+ display: none;
+ }
+
+ .ref::selection {
+ color: $placeholder-text-color;
+ }
+
+ .dropdown {
+ .dropdown-menu-toggle {
+ min-width: 285px;
+ }
+
+ .dropdown-select {
+ width: 285px;
+ }
+ }
.btn-group:not(.hide) {
display: flex;
@@ -214,15 +231,16 @@ ul.related-merge-requests > li {
flex-shrink: 0;
}
- .dropdown-menu {
+ .create-merge-request-dropdown-menu {
width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
display: none;
+ margin-top: 4px;
}
- .dropdown-toggle {
+ .create-merge-request-dropdown-toggle {
.fa-caret-down {
pointer-events: none;
color: inherit;
@@ -230,18 +248,50 @@ ul.related-merge-requests > li {
}
}
+ .droplab-item-ignore {
+ pointer-events: auto;
+ }
+
+ .create-item {
+ cursor: pointer;
+ margin: 0 1px;
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+ }
+ }
+
+ li.divider {
+ margin: 8px 10px;
+ }
+
li:not(.divider) {
+ padding: 8px 9px;
+
+ &:last-child {
+ padding-bottom: 8px;
+ }
+
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
+
+ .description {
+ display: block;
+ }
+ }
+
+ &.droplab-item-ignore {
+ padding-top: 8px;
}
.icon-container {
float: left;
- padding-left: 6px;
i {
visibility: hidden;
@@ -249,13 +299,12 @@ ul.related-merge-requests > li {
}
.description {
- padding-left: 30px;
- font-size: 13px;
+ padding-left: 22px;
+ }
- strong {
- display: block;
- font-weight: $gl-font-weight-bold;
- }
+ input,
+ span {
+ margin: 4px 0 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index b7985c4dea5..b2250a1ce2f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -252,6 +252,10 @@
background: $white-light;
}
+ .login-page-broadcast {
+ margin-top: 50px;
+ }
+
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 2c83b30500d..2dc0c288a6d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -395,6 +395,18 @@
}
}
}
+
+ .clone-dropdown-btn {
+ background-color: $white-light;
+ }
+
+ .clone-options-dropdown {
+ min-width: 240px;
+
+ .dropdown-menu-inner-content {
+ min-width: 320px;
+ }
+ }
}
.project-repo-buttons {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index d93c51d5448..402412eae71 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -35,270 +35,276 @@
}
}
-.repository-view {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- color: $almost-black;
+.multi-file {
+ display: flex;
+ height: calc(100vh - 145px);
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
+ }
+}
- .code.white pre .hll {
- background-color: $well-light-border !important;
+.ide-file-list {
+ flex: 1;
+ overflow: scroll;
+
+ .file {
+ cursor: pointer;
}
- .tree-content-holder {
- display: -webkit-flex;
- display: flex;
- min-height: 300px;
+ a {
+ color: $gl-text-color;
}
- .tree-content-holder-mini {
- height: 100vh;
+ th {
+ position: sticky;
+ top: 0;
}
+}
- .panel-right {
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: column;
- flex-direction: column;
- width: 80%;
- height: 100%;
+.multi-file-table-name,
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 0;
+}
- .monaco-editor.vs {
- .current-line {
- border: 0;
- background: $well-light-border;
- }
+.multi-file-table-name {
+ width: 350px;
+}
- .line-numbers {
- cursor: pointer;
+.multi-file-table-col-commit-message {
+ width: 50%;
+}
- &:hover {
- text-decoration: underline;
- }
- }
- }
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
- .blob-no-preview {
- .vertical-center {
- justify-content: center;
- width: 100%;
- }
- }
+.multi-file-tabs {
+ display: flex;
+ overflow: scroll;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
- &.blob-editor-container {
- overflow: hidden;
- }
+ > li {
+ position: relative;
+ }
+}
- .blob-viewer-container {
- -webkit-flex: 1;
- flex: 1;
- overflow: auto;
-
- > div,
- .file-content:not(.wiki) {
- display: flex;
- }
-
- > div,
- .file-content,
- .blob-viewer,
- .line-number,
- .blob-content,
- .code {
- min-height: 100%;
- width: 100%;
- }
-
- .line-numbers {
- min-width: 44px;
- }
-
- .blob-content {
- flex: 1;
- overflow-x: auto;
- }
- }
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
- #tabs {
- position: relative;
- flex-shrink: 0;
- display: flex;
- width: 100%;
- padding-left: 0;
- margin-bottom: 0;
- white-space: nowrap;
- overflow-y: hidden;
- overflow-x: auto;
-
- li {
- position: relative;
- background: $gray-normal;
- padding: #{$gl-padding / 2} $gl-padding;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- cursor: pointer;
-
- &.active {
- background: $white-light;
- border-bottom: 0;
- }
-
- a {
- @include str-truncated(100px);
- color: $gl-text-color;
- vertical-align: middle;
- text-decoration: none;
- margin-right: 12px;
-
- &:focus {
- outline: none;
- }
- }
-
- .close-btn {
- position: absolute;
- right: 8px;
- top: 50%;
- padding: 0;
- background: none;
- border: 0;
- font-size: $gl-font-size;
- transform: translateY(-50%);
- }
-
- .close-icon:hover {
- color: $hint-color;
- }
-
- .close-icon,
- .unsaved-icon {
- color: $gray-darkest;
- }
-
- .unsaved-icon {
- color: $brand-success;
- }
-
- &.tabs-divider {
- width: 100%;
- background-color: $white-light;
- border-right: 0;
- border-top-right-radius: 2px;
- }
- }
- }
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: $gl-font-size;
+ color: $gray-darkest;
+ transform: translateY(-50%);
+
+ &:not(.modified):hover,
+ &:not(.modified):focus {
+ color: $hint-color;
+ }
- .repo-file-buttons {
- background-color: $white-light;
- padding: 5px 10px;
- border-top: 1px solid $white-normal;
- }
+ &.modified {
+ color: $indigo-700;
+ }
+}
- #binary-viewer {
- height: 80vh;
- overflow: auto;
- margin: 0;
-
- .blob-viewer {
- padding-top: 20px;
- padding-left: 20px;
- }
-
- .binary-unknown {
- text-align: center;
- padding-top: 100px;
- background: $gray-light;
- height: 100%;
- font-size: 17px;
-
- span {
- display: block;
- }
- }
- }
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.multi-file-editor-btn-group {
+ padding: $grid-size;
+ border-top: 1px solid $white-dark;
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .blob-viewer {
+ height: 100%;
}
- #commit-area {
- background: $gray-light;
- padding: 20px;
+ .file-content.code {
+ display: flex;
- .help-block {
- padding-top: 7px;
- margin-top: 0;
+ i {
+ margin-left: -10px;
}
}
- #view-toggler {
- height: 41px;
- position: relative;
- display: block;
- border-bottom: 1px solid $white-normal;
- background: $white-light;
- margin-top: -5px;
+ .line-numbers {
+ min-width: 50px;
}
- #binary-viewer {
- img {
- max-width: 100%;
- }
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
}
+}
- #sidebar {
- flex: 1;
- height: 100%;
+.multi-file-commit-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 290px;
+ padding: $gl-padding;
+ background-color: $gray-light;
+ border-left: 1px solid $white-dark;
+
+ &.is-collapsed {
+ width: 60px;
+ padding: 0;
+ }
+}
- &.sidebar-mini {
- width: 20%;
- border-right: 1px solid $white-normal;
- overflow: auto;
- }
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
- .table {
- margin-bottom: 0;
- }
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ padding: 0 0 12px;
+ margin-bottom: 12px;
+ border-bottom: 1px solid $white-dark;
- tr {
- .repo-file-options {
- padding: 2px 16px;
- width: 100%;
- }
-
- .title {
- font-size: 10px;
- text-transform: uppercase;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
- }
-
- .file-icon {
- margin-right: 5px;
- }
-
- td {
- white-space: nowrap;
- }
- }
+ &.is-collapsed {
+ border-bottom: 1px solid $white-dark;
- .file {
- cursor: pointer;
+ svg {
+ margin-left: auto;
+ margin-right: auto;
}
+ }
+}
- a {
- @include str-truncated(250px);
- color: $almost-black;
- }
+.multi-file-commit-panel-collapse-btn {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-left: auto;
+ font-size: 20px;
+
+ &.is-collapsed {
+ margin-right: auto;
+ }
+}
+
+.multi-file-commit-list {
+ flex: 1;
+ overflow: scroll;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ align-items: center;
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
}
}
-.render-error {
- min-height: calc(100vh - 62px);
+.multi-file-commit-list-path {
+ @include str-truncated(100%);
+}
+
+.multi-file-commit-form {
+ padding-top: 12px;
+ border-top: 1px solid $white-dark;
+}
+
+.multi-file-commit-fieldset {
+ display: flex;
+ align-items: center;
+ padding-bottom: 12px;
- p {
- width: 100%;
+ .btn {
+ flex: 1;
}
}
-.multi-file-table-col-name {
- width: 350px;
+.multi-file-commit-message.form-control {
+ height: 80px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, .5);
+ }
+ }
}
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index f6d9f88032f..744e448e8df 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -54,7 +54,7 @@ module IssuableActions
end
def destroy
- Issuable::DestroyService.new(project, current_user).execute(issuable)
+ Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index f28df83d5a5..56df9991fda 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -41,7 +41,7 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+ redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present?
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 28fee0465d5..d7a3441a245 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create_merge_request
- result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
+ create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
+ result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index d60a24d3f1d..9f966889995 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 5bb84984142..dccde46fa33 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -30,9 +30,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
- ssh_clone_button(project, 'bottom', append_link: false)
+ ssh_clone_button(project, append_link: false)
else
- http_clone_button(project, 'bottom', append_link: false)
+ http_clone_button(project, append_link: false)
end
end
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 069c29feb80..ec6194d204f 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -26,7 +26,7 @@ module AutoDevopsHelper
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
- missing_service = !project.kubernetes_service&.active?
+ missing_service = !project.deployment_platform&.active?
if missing_service
params = {
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 8e8feeea1d8..d06cf2de2c3 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -56,42 +56,36 @@ module ButtonHelper
end
end
- def http_clone_button(project, placement = 'right', append_link: true)
- klass = 'http-selector'
- klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
-
+ def http_clone_button(project, append_link: true)
protocol = gitlab_config.protocol.upcase
+ dropdown_description = http_dropdown_description(protocol)
+ append_url = project.http_url_to_repo if append_link
+
+ dropdown_item_with_description(protocol, dropdown_description, href: append_url)
+ end
+
+ def http_dropdown_description(protocol)
+ if current_user.try(:require_password_creation_for_git?)
+ _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ else
+ _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ end
+ end
- tooltip_title =
- if current_user.try(:require_password_creation_for_git?)
- _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
- else
- _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
- end
+ def ssh_clone_button(project, append_link: true)
+ dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
+ append_url = project.ssh_url_to_repo if append_link
- content_tag (append_link ? :a : :span), protocol,
- class: klass,
- href: (project.http_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: tooltip_title
- }
+ dropdown_item_with_description('SSH', dropdown_description, href: append_url)
end
- def ssh_clone_button(project, placement = 'right', append_link: true)
- klass = 'ssh-selector'
- klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
+ def dropdown_item_with_description(title, description, href: nil)
+ button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
+ button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
- content_tag (append_link ? :a : :span), 'SSH',
- class: klass,
- href: (project.ssh_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: _('Add an SSH key to your profile to pull or push via SSH.')
- }
+ content_tag (href ? :a : :span),
+ button_content,
+ class: "#{title.downcase}-selector",
+ href: (href if href)
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ebbefc51a4f..fd64670f6b0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -365,7 +365,7 @@ module Ci
end
def has_kubernetes_active?
- project.kubernetes_service&.active?
+ project.deployment_platform&.active?
end
def has_stage_seeds?
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 185d9473aab..6d7fb4b7dbf 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -17,8 +17,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- # We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes'
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
@@ -29,15 +28,9 @@ module Clusters
validates :name, cluster_name: true
validate :restrict_modification, on: :update
- # TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
- # We need callback here because `enabled` belongs to Clusters::Cluster
- # Callbacks in Clusters::Platforms::Kubernetes will not be called after update
- after_save :update_kubernetes_integration!
-
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
- delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 6dc1ee810d3..7ab670cf1ef 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -1,7 +1,12 @@
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
+ include Gitlab::CurrentSettings
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
self.table_name = 'cluster_platforms_kubernetes'
+ self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
@@ -29,19 +34,14 @@ module Clusters
validates :api_url, url: true, presence: true
validates :token, presence: true
- # TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
- after_destroy :destroy_kubernetes_integration!
+ after_save :clear_reactive_cache!
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
- class << self
- def namespace_for_project(project)
- "#{project.path}-#{project.id}"
- end
- end
+ alias_method :active?, :enabled?
def actual_namespace
if namespace.present?
@@ -51,58 +51,127 @@ module Clusters
end
end
- def default_namespace
- self.class.namespace_for_project(project) if project
+ def predefined_variables
+ config = YAML.dump(kubeconfig)
+
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
+ { key: 'KUBECONFIG', value: config, public: false, file: true }
+ ]
+
+ if ca_pem.present?
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
+ variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ end
+
+ variables
end
- def kubeclient
- @kubeclient ||= kubernetes_service.kubeclient if manages_kubernetes_service?
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = filter_by_label(data[:pods], app: environment.slug)
+ terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
+ terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ end
end
- def update_kubernetes_integration!
- raise 'Kubernetes service already configured' unless manages_kubernetes_service?
+ # Caches resources in the namespace so other calls don't need to block on
+ # network access
+ def calculate_reactive_cache
+ return unless enabled? && project && !project.pending_delete?
- # This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
- cluster.reload
+ # We may want to cache extra things in the future
+ { pods: read_pods }
+ end
- ensure_kubernetes_service&.update!(
- active: enabled?,
- api_url: api_url,
- namespace: namespace,
+ def kubeclient
+ @kubeclient ||= build_kubeclient!
+ end
+
+ private
+
+ def kubeconfig
+ to_kubeconfig(
+ url: api_url,
+ namespace: actual_namespace,
token: token,
- ca_pem: ca_cert
- )
+ ca_pem: ca_pem)
+ end
+
+ def default_namespace
+ return unless project
+
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def active?
- manages_kubernetes_service?
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && actual_namespace
+
+ unless (username && password) || token
+ raise "Either username/password or token is required to access API"
+ end
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
end
- private
+ # Returns a hash of all pods in the namespace
+ def read_pods
+ kubeclient = build_kubeclient!
- def enforce_namespace_to_lower_case
- self.namespace = self.namespace&.downcase
+ kubeclient.get_pods(namespace: actual_namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+
+ []
end
- # TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
- def manages_kubernetes_service?
- return true unless kubernetes_service&.active?
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
- kubernetes_service.api_url == api_url
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
end
- def destroy_kubernetes_integration!
- return unless manages_kubernetes_service?
+ def kubeclient_auth_options
+ { bearer_token: token }
+ end
+
+ def join_api_url(api_path)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [prefix, api_path].join("/")
- kubernetes_service&.destroy!
+ url.to_s
end
- def kubernetes_service
- @kubernetes_service ||= project&.kubernetes_service
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
end
- def ensure_kubernetes_service
- @kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
end
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 21a028e351c..bf69b4c50f0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -138,11 +138,11 @@ class Environment < ActiveRecord::Base
end
def has_terminals?
- project.deployment_service.present? && available? && last_deployment.present?
+ project.deployment_platform.present? && available? && last_deployment.present?
end
def terminals
- project.deployment_service.terminals(self) if has_terminals?
+ project.deployment_platform.terminals(self) if has_terminals?
end
def has_metrics?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a6b10d5349c..bbc01e9677c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -283,9 +283,9 @@ class MergeRequest < ActiveRecord::Base
if persisted?
merge_request_diff.commit_shas
elsif compare_commits
- compare_commits.reverse.map(&:sha)
+ compare_commits.to_a.reverse.map(&:sha)
else
- []
+ Array(diff_head_sha)
end
end
@@ -494,7 +494,7 @@ class MergeRequest < ActiveRecord::Base
def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
- diffs = merge_request_diffs.viewable.select_without_diff
+ diffs = merge_request_diffs.viewable
h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha)
@@ -899,7 +899,8 @@ class MergeRequest < ActiveRecord::Base
def compute_diverged_commits_count
return 0 unless source_branch_sha && target_branch_sha
- Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
+ target_project.repository
+ .count_commits_between(source_branch_sha, target_branch_sha)
end
private :compute_diverged_commits_count
@@ -918,28 +919,18 @@ class MergeRequest < ActiveRecord::Base
# Note that this could also return SHA from now dangling commits
#
def all_commit_shas
- if persisted?
- # MySQL doesn't support LIMIT in a subquery.
- diffs_relation =
- if Gitlab::Database.postgresql?
- merge_request_diffs.order(id: :desc).limit(100)
- else
- merge_request_diffs
- end
+ return commit_shas unless persisted?
- column_shas = MergeRequestDiffCommit
- .where(merge_request_diff: diffs_relation)
- .limit(10_000)
- .pluck('sha')
+ diffs_relation = merge_request_diffs
- serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
+ # MySQL doesn't support LIMIT in a subquery.
+ diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
- (column_shas + serialised_shas).uniq
- elsif compare_commits
- compare_commits.to_a.reverse.map(&:id)
- else
- [diff_head_sha]
- end
+ MergeRequestDiffCommit
+ .where(merge_request_diff: diffs_relation)
+ .limit(10_000)
+ .pluck('sha')
+ .uniq
end
def merge_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 9ee561b5161..c37aa0a594b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,14 +1,14 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include Gitlab::EncodingHelper
include ManualInverseAssociation
+ include IgnorableColumn
- # Prevent store of diff if commits amount more then 500
+ # Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
- # Valid types of serialized diffs allowed by Gitlab::Git::Diff
- VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
+ ignore_column :st_commits,
+ :st_diffs
belongs_to :merge_request
manual_inverse_association :merge_request, :merge_request_diff
@@ -16,9 +16,6 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
- serialize :st_commits # rubocop:disable Cop/ActiveRecordSerialize
- serialize :st_diffs # rubocop:disable Cop/ActiveRecordSerialize
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -32,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base
scope :viewable, -> { without_state(:empty) }
+ scope :recent, -> { order(id: :desc).limit(100) }
+
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
@@ -40,14 +39,6 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
- def self.select_without_diff
- select(column_names - ['st_diffs'])
- end
-
- def st_commits
- super || []
- end
-
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -129,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commit_shas
- if st_commits.present?
- st_commits.map { |commit| commit[:id] }
- else
- merge_request_diff_commits.map(&:sha)
- end
+ merge_request_diff_commits.map(&:sha)
end
def diff_refs=(new_diff_refs)
@@ -208,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_count
- if st_commits.present?
- st_commits.size
- else
- merge_request_diff_commits.size
- end
- end
-
- def utf8_st_diffs
- return [] if st_diffs.blank?
-
- st_diffs.map do |diff|
- diff.each do |k, v|
- diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
- end
- end
+ merge_request_diff_commits.size
end
private
- # Old GitLab implementations may have generated diffs as ["--broken-diff"].
- # Avoid an error 500 by ignoring bad elements. See:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
- def valid_raw_diff?(raw)
- return false unless raw.respond_to?(:each)
-
- raw.any? { |element| VALID_CLASSES.include?(element.class) }
- end
-
def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index|
diff_hash = diff.to_hash.merge(
@@ -259,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(options)
- return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database
-
- raw = diffs_from_database
+ raw = merge_request_diff_files.map(&:to_hash)
if paths = options[:paths]
raw = raw.select do |diff|
@@ -272,22 +234,8 @@ class MergeRequestDiff < ActiveRecord::Base
Gitlab::Git::DiffCollection.new(raw, options)
end
- def diffs_from_database
- return @diffs_from_database if defined?(@diffs_from_database)
-
- @diffs_from_database =
- if st_diffs.present?
- if valid_raw_diff?(st_diffs)
- st_diffs
- end
- elsif merge_request_diff_files.present?
- merge_request_diff_files.map(&:to_hash)
- end
- end
-
def load_commits
- commits = st_commits.presence || merge_request_diff_commits
- commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch)
diff --git a/app/models/project.rb b/app/models/project.rb
index 5a3f591c2e7..c6f7f56f311 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -897,12 +897,10 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
- def deployment_services
- services.where(category: :deployment)
- end
-
- def deployment_service
- @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ # TODO: This will be extended for multiple enviroment clusters
+ def deployment_platform
+ @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
+ @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end
def monitoring_services
@@ -1547,9 +1545,9 @@ class Project < ActiveRecord::Base
end
def deployment_variables
- return [] unless deployment_service
+ return [] unless deployment_platform
- deployment_service.predefined_variables
+ deployment_platform.predefined_variables
end
def auto_devops_variables
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index bc62972dbb0..b82567ce2b3 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,3 +1,8 @@
+##
+# NOTE:
+# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic.
+# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
+# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < DeploymentService
include Gitlab::CurrentSettings
include Gitlab::Kubernetes
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 89bfc5f9a9c..d28fed11ca8 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -10,7 +10,9 @@ class ProtectedBranch < ActiveRecord::Base
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
- self.matching(ref_name, protected_refs: project.protected_branches).present?
+ refs = project.protected_branches.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
def self.default_branch_protected?
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index f38109c0e52..42a9bcf7723 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -5,6 +5,8 @@ class ProtectedTag < ActiveRecord::Base
protected_ref_access_levels :create
def self.protected?(project, ref_name)
- self.matching(ref_name, protected_refs: project.protected_tags).present?
+ refs = project.protected_tags.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
end
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
index 99cc9a196e6..19873fe09a5 100644
--- a/app/services/base_count_service.rb
+++ b/app/services/base_count_service.rb
@@ -9,7 +9,7 @@ class BaseCountService
end
def count
- Rails.cache.fetch(cache_key, raw: raw?) { uncached_count }.to_i
+ Rails.cache.fetch(cache_key, cache_options) { uncached_count }.to_i
end
def refresh_cache
@@ -31,4 +31,10 @@ class BaseCountService
def cache_key
raise NotImplementedError, 'cache_key must be implemented and return a String'
end
+
+ # subclasses can override to add any specific options, such as
+ # super.merge({ expires_in: 5.minutes })
+ def cache_options
+ { raw: raw? }
+ end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 8223e5fed7f..c2fb01466df 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,6 +1,8 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
+ @issue_iid = params.delete(:issue_iid)
+
self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = []
merge_request.source_project = find_source_project
@@ -116,37 +118,53 @@ module MergeRequests
# more than one commit in the MR
#
def assign_title_and_description
- if match = source_branch.match(/\A(\d+)-/)
- iid = match[1]
- end
+ assign_title_and_description_from_single_commit
+ assign_title_from_issue
- commits = compare_commits
- if commits && commits.count == 1
- commit = commits.first
- merge_request.title = commit.title
- merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = target_project.get_issue(iid, current_user)
- case issue
- when Issue
- merge_request.title = "Resolve \"#{issue.title}\""
- when ExternalIssue
- merge_request.title = "Resolve #{issue.title}"
- end
+ merge_request.title ||= source_branch.titleize.humanize
+ merge_request.title = wip_title if compare_commits.empty?
+
+ append_closes_description
+ end
+
+ def append_closes_description
+ return unless issue_iid
+
+ closes_issue = "Closes ##{issue_iid}"
+
+ if description.present?
+ merge_request.description += closes_issue.prepend("\n\n")
else
- merge_request.title = source_branch.titleize.humanize
+ merge_request.description = closes_issue
end
+ end
- if iid
- closes_issue = "Closes ##{iid}"
+ def assign_title_and_description_from_single_commit
+ commits = compare_commits
+
+ return unless commits&.count == 1
+
+ commit = commits.first
+ merge_request.title ||= commit.title
+ merge_request.description ||= commit.description.try(:strip)
+ end
+
+ def assign_title_from_issue
+ return unless issue
- if description.present?
- merge_request.description += closes_issue.prepend("\n\n")
- else
- merge_request.description = closes_issue
+ merge_request.title =
+ case issue
+ when Issue then "Resolve \"#{issue.title}\""
+ when ExternalIssue then "Resolve #{issue.title}"
end
- end
+ end
+
+ def issue_iid
+ @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
+ end
- merge_request.title = wip_title if commits.empty?
+ def issue
+ @issue ||= target_project.get_issue(issue_iid, current_user)
end
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index da39a380451..89dab1dd028 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -1,7 +1,18 @@
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
+ def initialize(project, user, params)
+ # branch - the name of new branch
+ # ref - the source of new branch.
+
+ @branch_name = params[:branch_name]
+ @issue_iid = params[:issue_iid]
+ @ref = params[:ref]
+
+ super(project, user)
+ end
+
def execute
- return error('Invalid issue iid') unless issue_iid.present? && issue.present?
+ return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
params[:label_ids] = issue.label_ids if issue.label_ids.any?
@@ -21,20 +32,16 @@ module MergeRequests
private
- def issue_iid
- @isssue_iid ||= params.delete(:issue_iid)
- end
-
def issue
- @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
+ @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
end
def branch_name
- @branch_name ||= issue.to_branch_name
+ @branch ||= @branch_name || issue.to_branch_name
end
def ref
- project.default_branch || 'master'
+ @ref || project.default_branch || 'master'
end
def merge_request
@@ -43,6 +50,7 @@ module MergeRequests
def merge_request_params
{
+ issue_iid: @issue_iid,
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index e2aec532a9d..38741fe6662 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -5,7 +5,12 @@ xml.entry do
xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80)
xml.updated event.updated_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
+
+ # We're deliberately re-using "event.author" here since this data is
+ # eager-loaded. This allows us to re-use the user object's Email address,
+ # instead of having to run additional queries to figure out what Email to use
+ # for the avatar.
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author))
xml.author do
xml.username event.author_username
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 1db32379df3..05ddd0ec733 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,6 +1,8 @@
.flash-container.flash-container-page
-# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
- %div{ class: "flash-#{key}" }
- %div{ class: (container_class) }
- %span= value
+ -# Don't show a flash message if the message is nil
+ - if value
+ %div{ class: "flash-#{key}" }
+ %div{ class: (container_class) }
+ %span= value
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 52fb46eb8c9..97016d28535 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -4,7 +4,8 @@
%body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= render "layouts/header/empty"
- = render "layouts/broadcast"
+ .login-page-broadcast
+ = render "layouts/broadcast"
.container.navless-container
.content
= render "layouts/flash"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 66146e61263..2ce960df13c 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -266,7 +266,7 @@
Pages
- else
- = nav_link(path: %w[members#show]) do
+ = nav_link(controller: :project_members) do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
.nav-icon-container
= sprite_icon('users')
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 6b321f60212..665120c7e49 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -5,7 +5,7 @@
.col-sm-4
= render 'sidebar'
.col-sm-8
- - if @project.kubernetes_service&.active?
+ - if @project.deployment_platform&.active?
%h4.prepend-top-0= s_('ClusterIntegration|Cluster management')
%p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 5ebeae5c35f..71206f3a386 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -147,7 +147,7 @@
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
- - if @project.deployment_services.any?
+ - if @project.deployment_platform.present?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
new file mode 100644
index 00000000000..8a549d431ee
--- /dev/null
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -0,0 +1,26 @@
+- avatar = namespace_icon(namespace, 100)
+- can_create_project = current_user.can?(:create_projects, namespace)
+
+- if forked_project = namespace.find_fork_of(@project)
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
+ = link_to project_path(forked_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
+- else
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: ("disabled" unless can_create_project) }
+ = link_to project_forks_path(@project, namespace_key: namespace.id),
+ method: "POST",
+ class: ("disabled has-tooltip" unless can_create_project),
+ title: (_('You have reached your project limit') unless can_create_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index e9613534dde..475c6ba4d3d 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -14,22 +14,7 @@
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project
- @namespaces.each do |namespace|
- - avatar = namespace_icon(namespace, 100)
- - can_create_project = current_user.can?(:create_projects, namespace)
- - forked_project = namespace.find_fork_of(@project)
- - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
- .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
- = link_to fork_path,
- method: "POST",
- class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
- title: (_('You have reached your project limit') unless can_create_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- = project_identicon(namespace, class: "avatar s100 identicon")
- - else
- .avatar-container.s100
- = image_tag(avatar, class: "avatar s100")
- %h5.prepend-top-default
- = namespace.human_name
+ = render 'fork_button', namespace: namespace
- else
%strong
No available namespaces to fork the project.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 483f28c74f2..1eccc0509bd 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -13,5 +13,5 @@
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url,
last_fetched_at: Time.now.to_i,
- issue_data: serialize_issuable(@issue),
+ noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } }
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 4f78102be0c..331d62cf247 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,34 +1,50 @@
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
-- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
+- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- if can?(current_user, :push_code, @project)
- .create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
+ - can_create_path = can_create_branch_project_issue_path(@project, @issue)
+ - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
+ - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
+ - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
+
+ .create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
.btn-group.available.hide
- %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
- %button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
+ %button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
+ = value
+
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
- %ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+
+ %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a merge request
- %span
- Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
- %li.divider.droplab-item-ignore
- %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a branch
- %span
- Creates a branch named after this issue, from '#{@project.default_branch}'.
+ %li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create merge request and branch
+
+ %li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create branch
+ %li.divider
+
+ %li.droplab-item-ignore
+ Branch name
+ %input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
+ %span.js-branch-message.branch-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ Source (branch or tag)
+ %input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
+ %span.js-ref-message.ref-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ %button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
+ Create merge request
+
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 705a4607ad2..7a68aa16aa4 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -67,7 +67,7 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to _('Set up Koding'), add_koding_stack_path(@project)
- - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_platform.present?
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') }
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 745a6040488..64cc70053ef 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -11,6 +11,6 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
-%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3d9c90c38fe..fba08092351 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -7,7 +7,7 @@
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 5867ea58378..87e8c416194 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,3 +1,4 @@
+- @no_container = true;
#repo{ data: { root: @path.empty?.to_s,
root_url: project_tree_path(project),
url: content_url,
diff --git a/changelogs/unreleased/13634-broadcast-message.yml b/changelogs/unreleased/13634-broadcast-message.yml
new file mode 100644
index 00000000000..26c4c133443
--- /dev/null
+++ b/changelogs/unreleased/13634-broadcast-message.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broadcast message not showing up on login page
+merge_request: 15578
+author:
+type: fixed
diff --git a/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml b/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
new file mode 100644
index 00000000000..d4c209aaf2b
--- /dev/null
+++ b/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Add an ability to use a custom branch name on creation from issues
+merge_request: 13884
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml b/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
new file mode 100644
index 00000000000..cd19d24e035
--- /dev/null
+++ b/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
@@ -0,0 +1,5 @@
+---
+title: added support for ordering and sorting in notes api
+merge_request: 15342
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml b/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
new file mode 100644
index 00000000000..cb522cb7611
--- /dev/null
+++ b/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Removed tooltip from clone dropdown
+merge_request: 15334
+author:
+type: other
diff --git a/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml b/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
new file mode 100644
index 00000000000..ebd7b582e8a
--- /dev/null
+++ b/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
@@ -0,0 +1,5 @@
+---
+title: Fix item name and namespace text overflow in Projects dropdown
+merge_request: 15451
+author:
+type: fixed
diff --git a/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml b/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
new file mode 100644
index 00000000000..00f7dd7c0f0
--- /dev/null
+++ b/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search results when a filename would contain a special character.
+merge_request: 15606
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml b/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml
deleted file mode 100644
index 1e3f52b3a9c..00000000000
--- a/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories
-merge_request: 15520
-author:
-type: fixed
diff --git a/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml b/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml
deleted file mode 100644
index 0ccbc699729..00000000000
--- a/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories
-merge_request: 15600
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-double-fork.yml b/changelogs/unreleased/bvl-double-fork.yml
new file mode 100644
index 00000000000..50bc1adde26
--- /dev/null
+++ b/changelogs/unreleased/bvl-double-fork.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly link to a forked project from the new fork page.
+merge_request: 15653
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
new file mode 100644
index 00000000000..2acb98db785
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Create a fork network for forks with a deleted source
+merge_request: 15595
+author:
+type: fixed
diff --git a/changelogs/unreleased/default-values-for-mr-states.yml b/changelogs/unreleased/default-values-for-mr-states.yml
deleted file mode 100644
index f873a5335d0..00000000000
--- a/changelogs/unreleased/default-values-for-mr-states.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix defaults for MR states and merge statuses
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml b/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml
deleted file mode 100644
index be687fda147..00000000000
--- a/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pulling and pushing using a personal access token with the sudo scope
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-project-search-performance.yml b/changelogs/unreleased/dm-project-search-performance.yml
deleted file mode 100644
index b533043b163..00000000000
--- a/changelogs/unreleased/dm-project-search-performance.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Drastically improve project search performance by no longer searching namespace
- name
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-import-uploads-hashed-storage.yml b/changelogs/unreleased/fix-import-uploads-hashed-storage.yml
deleted file mode 100644
index d43cabbfb8f..00000000000
--- a/changelogs/unreleased/fix-import-uploads-hashed-storage.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix hashed storage for Import/Export uploads
-merge_request: 15482
-author:
-type: fixed
diff --git a/changelogs/unreleased/issue_40374.yml b/changelogs/unreleased/issue_40374.yml
deleted file mode 100644
index 73b48b890fe..00000000000
--- a/changelogs/unreleased/issue_40374.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix WIP system note not being created
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jk-group-mentions-fix.yml b/changelogs/unreleased/jk-group-mentions-fix.yml
deleted file mode 100644
index a28e3a87b6d..00000000000
--- a/changelogs/unreleased/jk-group-mentions-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix link text from group context
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml b/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml
deleted file mode 100644
index 7f6adfb4fd8..00000000000
--- a/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/protected-branches-names.yml b/changelogs/unreleased/protected-branches-names.yml
new file mode 100644
index 00000000000..3c6767df571
--- /dev/null
+++ b/changelogs/unreleased/protected-branches-names.yml
@@ -0,0 +1,5 @@
+---
+title: Only load branch names for protected branch checks
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/use-count_commits-directly.yml b/changelogs/unreleased/use-count_commits-directly.yml
new file mode 100644
index 00000000000..549e0744ea4
--- /dev/null
+++ b/changelogs/unreleased/use-count_commits-directly.yml
@@ -0,0 +1,5 @@
+---
+title: Improve the performance for counting commits
+merge_request: 15628
+author:
+type: performance
diff --git a/config/webpack.config.js b/config/webpack.config.js
index f7a7182a627..78ced4c3e8c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -117,6 +117,10 @@ var config = {
options: { limit: 2048 },
},
{
+ test: /\_worker\.js$/,
+ loader: 'worker-loader',
+ },
+ {
test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
exclude: /node_modules/,
loader: 'file-loader',
diff --git a/db/fixtures/test/01_admin.rb b/db/fixtures/test/01_admin.rb
new file mode 100644
index 00000000000..6f241f6fa4a
--- /dev/null
+++ b/db/fixtures/test/01_admin.rb
@@ -0,0 +1,15 @@
+require './spec/support/sidekiq'
+
+Gitlab::Seeder.quiet do
+ User.seed do |s|
+ s.id = 1
+ s.name = 'Administrator'
+ s.email = 'admin@example.com'
+ s.notification_email = 'admin@example.com'
+ s.username = 'root'
+ s.password = '5iveL!fe'
+ s.admin = true
+ s.projects_limit = 100
+ s.confirmed_at = DateTime.now
+ end
+end
diff --git a/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb b/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb
new file mode 100644
index 00000000000..30cf08b29fc
--- /dev/null
+++ b/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb
@@ -0,0 +1,36 @@
+class CleanUpFromMergeRequestDiffsAndCommits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('DeserializeMergeRequestDiffsAndCommits')
+
+ # The literal '--- []\n' value is created by the import process and treated
+ # as null by the application, so we can ignore those - even if we were
+ # migrating, it wouldn't create any rows.
+ literal_prefix = Gitlab::Database.postgresql? ? 'E' : ''
+ non_empty = "
+ (st_commits IS NOT NULL AND st_commits != #{literal_prefix}'--- []\n')
+ OR
+ (st_diffs IS NOT NULL AND st_diffs != #{literal_prefix}'--- []\n')
+ ".squish
+
+ MergeRequestDiff.where(non_empty).each_batch(of: 500) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits.new.perform(*range)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 5cd9f3198e3..8f8d8f27410 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -3,8 +3,19 @@ class LimitsToMysql < ActiveRecord::Migration
def up
return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
- change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
- change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
+ # These columns were removed in 10.3, but this is called from two places:
+ # 1. A migration run after they were added, but before they were removed.
+ # 2. A rake task which can be run at any time.
+ #
+ # Because of item 2, we need these checks.
+ if column_exists?(:merge_request_diffs, :st_commits)
+ change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
+ end
+
+ if column_exists?(:merge_request_diffs, :st_diffs)
+ change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
+ end
+
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
end
diff --git a/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb b/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb
new file mode 100644
index 00000000000..3a7b2a7fac0
--- /dev/null
+++ b/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb
@@ -0,0 +1,10 @@
+class RemoveMergeRequestDiffStCommitsAndStDiffs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :merge_request_diffs, :st_commits, :text
+ remove_column :merge_request_diffs, :st_diffs, :text
+ end
+end
diff --git a/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb b/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb
new file mode 100644
index 00000000000..698df712c11
--- /dev/null
+++ b/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb
@@ -0,0 +1,17 @@
+class AddIndexOnMergeRequestDiffsMergeRequestIdAndId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:merge_request_diffs, [:merge_request_id, :id])
+ end
+
+ def down
+ if index_exists?(:merge_request_diffs, [:merge_request_id, :id])
+ remove_concurrent_index(:merge_request_diffs, [:merge_request_id, :id])
+ end
+ end
+end
diff --git a/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb b/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb
new file mode 100644
index 00000000000..038e4807000
--- /dev/null
+++ b/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb
@@ -0,0 +1,17 @@
+class RemoveIndexOnMergeRequestDiffsMergeRequestDiffId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_concurrent_index(:merge_request_diffs, :merge_request_id)
+ end
+ end
+
+ def down
+ add_concurrent_index(:merge_request_diffs, :merge_request_id)
+ end
+end
diff --git a/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb b/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb
new file mode 100644
index 00000000000..05430efe1f6
--- /dev/null
+++ b/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb
@@ -0,0 +1,27 @@
+class RescheduleForkNetworkCreation < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'PopulateForkNetworksRange'.freeze
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 15.seconds
+
+ disable_ddl_transaction!
+
+ class ForkedProjectLink < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'forked_project_links'
+ end
+
+ def up
+ say 'Populating the `fork_networks` based on existing `forked_project_links`'
+
+ queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1e524621b4f..effb2604af2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20171124132536) do
+ActiveRecord::Schema.define(version: 20171124150326) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1014,8 +1014,6 @@ ActiveRecord::Schema.define(version: 20171124132536) do
create_table "merge_request_diffs", force: :cascade do |t|
t.string "state"
- t.text "st_commits"
- t.text "st_diffs"
t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -1025,7 +1023,7 @@ ActiveRecord::Schema.define(version: 20171124132536) do
t.string "start_commit_sha"
end
- add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+ add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
create_table "merge_request_metrics", force: :cascade do |t|
t.integer "merge_request_id", null: false
diff --git a/doc/api/notes.md b/doc/api/notes.md
index e627369e17b..d02ef84d0bd 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -10,12 +10,15 @@ Gets a list of all notes for a single issue.
```
GET /projects/:id/issues/:issue_iid/notes
+GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `issue_iid` (required) - The IID of an issue
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `issue_iid` | integer | yes | The IID of an issue
+| `sort` | string | no | Return issue notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return issue notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
```json
[
@@ -133,12 +136,15 @@ Gets a list of all notes for a single snippet. Snippet notes are comments users
```
GET /projects/:id/snippets/:snippet_id/notes
+GET /projects/:id/snippets/:snippet_id/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `snippet_id` (required) - The ID of a project snippet
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `snippet_id` | integer | yes | The ID of a project snippet
+| `sort` | string | no | Return snippet notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return snippet notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
### Get single snippet note
@@ -231,12 +237,15 @@ Gets a list of all notes for a single merge request.
```
GET /projects/:id/merge_requests/:merge_request_iid/notes
+GET /projects/:id/merge_requests/:merge_request_iid/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `merge_request_iid` (required) - The IID of a project merge request
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `merge_request_iid` | integer | yes | The IID of a project merge request
+| `sort` | string | no | Return merge request notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return merge request notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
### Get single merge request note
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 4773b6773e8..ceff57276d2 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -163,7 +163,7 @@ Starting a project from a template needs this project to be exported. On a
up to date master branch with run:
```
-gdk run db
+gdk run
# In a new terminal window
bundle exec rake gitlab:update_project_templates
git checkout -b update-project-templates
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index c4830322fa8..05e0a64af18 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -37,7 +37,7 @@ when using the migration helper method
`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
similar to `add_column` except it updates existing rows in batches without
blocking access to the table being modified. See ["Adding Columns With Default
-Values"](migration_style_guide.html#adding-columns-with-default-values) for more
+Values"](migration_style_guide.md#adding-columns-with-default-values) for more
information on how to use this method.
## Dropping Columns
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 88000f4c7a9..570b0d5b22f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -367,6 +367,9 @@ sudo usermod -aG redis git
# Enable packfile bitmaps
sudo -u git -H git config --global repack.writeBitmaps true
+
+ # Enable push options
+ sudo -u git -H git config --global receive.advertisePushOptions true
# Configure Redis connection settings
sudo -u git -H cp config/resque.yml.example config/resque.yml
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 727ca13ebcf..07a700f7b64 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -35,7 +35,7 @@ In Google's side:
1. You should now be able to see a Client ID and Client secret. Note them down
or keep this page open as you will need them later.
-1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Google Cloud APIs > Container Engine API > Enable**
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Container Engine API > Enable**
On your GitLab server:
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 5896f8f72a0..eafdd28071d 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -731,8 +731,6 @@ X-Gitlab-Event: Merge Request Hook
"title": "MS-Viewport",
"created_at": "2013-12-03T17:23:34Z",
"updated_at": "2013-12-03T17:23:34Z",
- "st_commits": null,
- "st_diffs": null,
"milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 23b1c61cd16..1b8a84c9599 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.0 to current | 0.2.0 |
+| 10.3 to current | 0.2.1 |
+| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
| 9.2.0 | 0.1.7 |
| 8.17.0 | 0.1.6 |
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 3c4db8b9601..5a77b859113 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -3,21 +3,41 @@ require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :chrome
Capybara.register_driver :chrome do |app|
- extra_args = []
- extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
-
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- chromeOptions: {
- 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
}
)
- Capybara::Selenium::Driver
- .new(app, browser: :chrome, desired_capabilities: capabilities)
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument("window-size=1240,1400")
+
+ # Chrome won't work properly in a Docker container in sandbox mode
+ options.add_argument("no-sandbox")
+
+ # Run headless by default unless CHROME_HEADLESS specified
+ unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+ options.add_argument("headless")
+
+ # Chrome documentation says this flag is needed for now
+ # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
+ options.add_argument("disable-gpu")
+ end
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :chrome,
+ desired_capabilities: capabilities,
+ options: options
+ )
end
+Capybara.javascript_driver = :chrome
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = false
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4b3c473b0bb..d6dea4c30e3 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -2,8 +2,8 @@ module API
module Helpers
module InternalHelpers
SSH_GITALY_FEATURES = {
- 'git-receive-pack' => :ssh_receive_pack,
- 'git-upload-pack' => :ssh_upload_pack
+ 'git-receive-pack' => [:ssh_receive_pack, Gitlab::GitalyClient::MigrationStatus::OPT_IN],
+ 'git-upload-pack' => [:ssh_upload_pack, Gitlab::GitalyClient::MigrationStatus::OPT_OUT]
}.freeze
def wiki?
@@ -102,8 +102,8 @@ module API
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
- feature = SSH_GITALY_FEATURES[action]
- return unless feature && Gitlab::GitalyClient.feature_enabled?(feature)
+ feature, status = SSH_GITALY_FEATURES[action]
+ return unless feature && Gitlab::GitalyClient.feature_enabled?(feature, status: status)
{
repository: repository.gitaly_repository,
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index ceaaeca4046..3588dc85c9e 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -18,6 +18,10 @@ module API
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return notes sorted in `asc` or `desc` order.'
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
@@ -29,11 +33,12 @@ module API
# at the DB query level (which we cannot in that case), the current
# page can have less elements than :per_page even if
# there's more than one page.
+ raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
notes =
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(noteable.notes.with_metadata)
+ paginate(raw_notes)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
index f8508b5fbdf..a976cb4c243 100644
--- a/lib/gitlab/background_migration/populate_fork_networks_range.rb
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -1,30 +1,55 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
+ # This background migration is going to create all `fork_networks` and
+ # the `fork_network_members` for the roots of fork networks based on the
+ # existing `forked_project_links`.
+ #
+ # When the source of a fork is deleted, we will create the fork with the
+ # target project as the root. This way, when there are forks of the target
+ # project, they will be joined into the same fork network.
+ #
+ # When the `fork_networks` and memberships for the root projects are created
+ # the `CreateForkNetworkMembershipsRange` migration is scheduled. This
+ # migration will create the memberships for all remaining forks-of-forks
class PopulateForkNetworksRange
def perform(start_id, end_id)
- log("Creating fork networks for forked project links: #{start_id} - #{end_id}")
+ create_fork_networks_for_existing_projects(start_id, end_id)
+ create_fork_networks_for_missing_projects(start_id, end_id)
+ create_fork_networks_memberships_for_root_projects(start_id, end_id)
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY # rubocop:disable Metrics/LineLength
+ BackgroundMigrationWorker.perform_in(
+ delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]
+ )
+ end
+
+ def create_fork_networks_for_existing_projects(start_id, end_id)
+ log("Creating fork networks: #{start_id} - #{end_id}")
ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
INSERT INTO fork_networks (root_project_id)
SELECT DISTINCT forked_project_links.forked_from_project_id
FROM forked_project_links
+ -- Exclude the forks that are not the first level fork of a project
WHERE NOT EXISTS (
SELECT true
FROM forked_project_links inner_links
WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
)
+
+ /* Exclude the ones that are already created, in case the fork network
+ was already created for another fork of the project.
+ */
AND NOT EXISTS (
SELECT true
FROM fork_networks
WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id
)
+
+ -- Only create a fork network for a root project that still exists
AND EXISTS (
SELECT true
FROM projects
@@ -32,7 +57,45 @@ module Gitlab
)
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
INSERT_NETWORKS
+ end
+
+ def create_fork_networks_for_missing_projects(start_id, end_id)
+ log("Creating fork networks with missing root: #{start_id} - #{end_id}")
+ ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
+ INSERT INTO fork_networks (root_project_id)
+ SELECT DISTINCT forked_project_links.forked_to_project_id
+
+ FROM forked_project_links
+
+ -- Exclude forks that are not the root forks
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM forked_project_links inner_links
+ WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ )
+ /* Exclude the ones that are already created, in case this migration is
+ re-run
+ */
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_networks
+ WHERE forked_project_links.forked_to_project_id = fork_networks.root_project_id
+ )
+
+ /* Exclude projects for which the project still exists, those are
+ Processed in the previous step of this migration
+ */
+ AND NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_NETWORKS
+ end
+
+ def create_fork_networks_memberships_for_root_projects(start_id, end_id)
log("Creating memberships for root projects: #{start_id} - #{end_id}")
ActiveRecord::Base.connection.execute <<~INSERT_ROOT
@@ -41,8 +104,12 @@ module Gitlab
FROM fork_networks
+ /* Joining both on forked_from- and forked_to- so we could create the
+ memberships for forks for which the source was deleted
+ */
INNER JOIN forked_project_links
ON forked_project_links.forked_from_project_id = fork_networks.root_project_id
+ OR forked_project_links.forked_to_project_id = fork_networks.root_project_id
WHERE NOT EXISTS (
SELECT true
@@ -51,9 +118,6 @@ module Gitlab
)
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
INSERT_ROOT
-
- delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
- BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id])
end
def log(message)
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 2479b4a7706..9230894877f 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -3,7 +3,6 @@ module Gitlab
class PlanEventFetcher < BaseEventFetcher
def initialize(*args)
@projections = [mr_diff_table[:id],
- mr_diff_table[:st_commits],
issue_metrics_table[:first_mentioned_in_commit_at]]
super(*args)
@@ -37,12 +36,7 @@ module Gitlab
def first_time_reference_commit(event)
return nil unless event && merge_request_diff_commits
- commits =
- if event['st_commits'].present?
- YAML.load(event['st_commits'])
- else
- merge_request_diff_commits[event['id'].to_i]
- end
+ commits = merge_request_diff_commits[event['id'].to_i]
return nil if commits.blank?
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index ddd52136bc4..228d97a87ab 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -49,6 +49,7 @@ module Gitlab
# Keep in mind that this method may allocate a lot of memory. It is up
# to the caller to limit the number of blobs and blob_size_limit.
#
+ # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798
def batch(repository, blob_references, blob_size_limit: nil)
blob_size_limit ||= MAX_DATA_DISPLAY_SIZE
blob_references.map do |sha, path|
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index d399636bb28..fb9c3e92d3f 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -505,7 +505,7 @@ module Gitlab
# Counts the amount of commits between `from` and `to`.
def count_commits_between(from, to)
- Commit.between(self, from, to).size
+ count_commits(ref: "#{from}..#{to}")
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 50ee879129c..2066005dddc 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.0'.freeze
+ VERSION = '0.2.1'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 263599831bf..f2b193c79cb 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -133,8 +133,6 @@ methods:
- :type
services:
- :type
- merge_request_diff:
- - :utf8_st_diffs
merge_request_diff_files:
- :utf8_diff
merge_requests:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 2b34ceb5831..d7d1b05e8b9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -58,7 +58,6 @@ module Gitlab
def setup_models
case @relation_name
- when :merge_request_diff then setup_st_diff_commits
when :merge_request_diff_files then setup_diff
when :notes then setup_note
when :project_label, :project_labels then setup_label
@@ -208,13 +207,6 @@ module Gitlab
relation_class: relation_class)
end
- def setup_st_diff_commits
- @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
-
- HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
- HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
- end
-
def setup_diff
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 561aa9e162c..e2662fc362b 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -47,8 +47,11 @@ module Gitlab
startline = 0
result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
+ matches = line.match(/^(?<ref>[^:]*):(?<filename>.*):(?<startline>\d+):/)
+ if matches
+ ref = matches[:ref]
+ filename = matches[:filename]
+ startline = matches[:startline]
startline = startline.to_i - index
extname = Regexp.escape(File.extname(filename))
basename = filename.sub(/#{extname}$/, '')
diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
index 7ac6162b54d..5cddc96a643 100644
--- a/lib/gitlab/prometheus/queries/query_additional_metrics.rb
+++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
@@ -76,7 +76,7 @@ module Gitlab
timeframe_start: timeframe_start,
timeframe_end: timeframe_end,
ci_environment_slug: environment.slug,
- kube_namespace: environment.project.kubernetes_service&.actual_namespace || '',
+ kube_namespace: environment.project.deployment_platform&.actual_namespace || '',
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
end
diff --git a/package.json b/package.json
index 8c1b2c401ed..c0a4db122bd 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"bootstrap-sass": "^3.3.6",
"brace-expansion": "^1.1.8",
"classlist-polyfill": "^1.2.0",
+ "clipboard": "^1.7.1",
"compression-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
@@ -33,6 +34,7 @@
"css-loader": "^0.28.0",
"d3": "^3.5.11",
"deckar01-task_list": "^2.0.0",
+ "diff": "^3.4.0",
"document-register-element": "1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
@@ -74,7 +76,8 @@
"vuex": "^3.0.1",
"webpack": "^3.5.5",
"webpack-bundle-analyzer": "^2.8.2",
- "webpack-stats-plugin": "^0.1.5"
+ "webpack-stats-plugin": "^0.1.5",
+ "worker-loader": "^1.1.0"
},
"devDependencies": {
"@gitlab-org/gitlab-svgs": "^1.1.1",
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index dd603eec7f6..8e05eca8d7e 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,7 +1,8 @@
#!/usr/bin/env ruby
gitaly_dir = 'tmp/tests/gitaly'
+env = { 'HOME' => File.expand_path('tmp/tests') }
args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
# Print the PID of the spawned process
-puts spawn(*args, [:out, :err] => 'log/gitaly-test.log')
+puts spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 973d6fed288..d731200f70f 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -113,22 +113,38 @@ describe Projects::BranchesController do
expect(response).to redirect_to project_tree_path(project, branch)
end
- it 'redirects to autodeploy setup page' do
- result = { status: :success, branch: double(name: branch) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response.location).to include(project_new_blob_path(project, branch))
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
- project.services << build(:kubernetes_service)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ before do
+ project.services << build(:kubernetes_service)
+ end
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
- expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ end
- expect(response.location).to include(project_new_blob_path(project, branch))
- expect(response).to have_gitlab_http_status(302)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 4a7c3e4f1ab..7a395f62511 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -4,52 +4,74 @@ describe 'Auto deploy' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- before do
- create :kubernetes_service, project: project
- project.team << [user, :master]
- sign_in user
- end
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'when no deployment service is active' do
+ before do
+ trun_off
+ end
- context 'when no deployment service is active' do
- before do
- project.kubernetes_service.update!(active: false)
+ it 'does not show a button to set up auto deploy' do
+ visit project_path(project)
+ expect(page).to have_no_content('Set up auto deploy')
+ end
end
- it 'does not show a button to set up auto deploy' do
- visit project_path(project)
- expect(page).to have_no_content('Set up auto deploy')
+ context 'when a deployment service is active' do
+ before do
+ trun_on
+ visit project_path(project)
+ end
+
+ it 'shows a button to set up auto deploy' do
+ expect(page).to have_link('Set up auto deploy')
+ end
+
+ it 'includes OpenShift as an available template', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+
+ within '.gitlab-ci-yml-selector' do
+ expect(page).to have_content('OpenShift')
+ end
+ end
+
+ it 'creates a merge request using "auto-deploy" branch', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+ within '.gitlab-ci-yml-selector' do
+ click_on 'OpenShift'
+ end
+ wait_for_requests
+ click_button 'Commit changes'
+
+ expect(page).to have_content('New Merge Request From auto-deploy into master')
+ end
end
end
- context 'when a deployment service is active' do
+ context 'when user configured kubernetes from Integration > Kubernetes' do
before do
- project.kubernetes_service.update!(active: true)
- visit project_path(project)
+ create :kubernetes_service, project: project
+ project.team << [user, :master]
+ sign_in user
end
- it 'shows a button to set up auto deploy' do
- expect(page).to have_link('Set up auto deploy')
- end
+ let(:trun_on) { project.deployment_platform.update!(active: true) }
+ let(:trun_off) { project.deployment_platform.update!(active: false) }
- it 'includes OpenShift as an available template', :js do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
- within '.gitlab-ci-yml-selector' do
- expect(page).to have_content('OpenShift')
- end
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ project.team << [user, :master]
+ sign_in user
end
- it 'creates a merge request using "auto-deploy" branch', :js do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
- within '.gitlab-ci-yml-selector' do
- click_on 'OpenShift'
- end
- wait_for_requests
- click_button 'Commit changes'
+ let(:trun_on) { project.deployment_platform.cluster.update!(enabled: true) }
+ let(:trun_off) { project.deployment_platform.cluster.update!(enabled: false) }
- expect(page).to have_content('New Merge Request From auto-deploy into master')
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
index 7ea29ff252b..ecbe51a7bc2 100644
--- a/spec/features/issuables/discussion_lock_spec.rb
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -14,7 +14,7 @@ describe 'Discussion Lock', :js do
project.add_developer(user)
end
- context 'when the discussion is unlocked' do
+ context 'when the discussion is unlocked' do
it 'the user can lock the issue' do
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
deleted file mode 100644
index edea95c6699..00000000000
--- a/spec/features/issues/create_branch_merge_request_spec.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-require 'rails_helper'
-
-feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
-
- context 'for team members' do
- before do
- project.team << [user, :developer]
- sign_in(user)
- end
-
- it 'allows creating a merge request from the issue page' do
- visit project_issue_path(project, issue)
-
- perform_enqueued_jobs do
- select_dropdown_option('create-mr')
-
- expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
- expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
-
- wait_for_requests
- end
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_content("created branch 1-cherry-coloured-funk")
- expect(page).to have_content("mentioned in merge request !1")
- end
-
- it 'allows creating a branch from the issue page' do
- visit project_issue_path(project, issue)
-
- select_dropdown_option('create-branch')
-
- wait_for_requests
-
- expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
- expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
- end
-
- context "when there is a referenced merge request" do
- let!(:note) do
- create(:note, :on_issue, :system, project: project, noteable: issue,
- note: "mentioned in #{referenced_mr.to_reference}")
- end
-
- let(:referenced_mr) do
- create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes #{issue.to_reference}", author: user)
- end
-
- before do
- referenced_mr.cache_merge_request_closes_issues!(user)
-
- visit project_issue_path(project, issue)
- end
-
- it 'disables the create branch button' do
- expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
- expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
- expect(page).to have_content /1 Related Merge Request/
- end
- end
-
- context 'when merge requests are disabled' do
- before do
- project.project_feature.update(merge_requests_access_level: 0)
-
- visit project_issue_path(project, issue)
- end
-
- it 'shows only create branch button' do
- expect(page).not_to have_button('Create a merge request')
- expect(page).to have_button('Create a branch')
- end
- end
-
- context 'when issue is confidential' do
- it 'disables the create branch button' do
- issue = create(:issue, :confidential, project: project)
-
- visit project_issue_path(project, issue)
-
- expect(page).not_to have_css('.create-mr-dropdown-wrap')
- end
- end
- end
-
- context 'for visitors' do
- before do
- visit project_issue_path(project, issue)
- end
-
- it 'shows no buttons' do
- expect(page).not_to have_selector('.create-mr-dropdown-wrap')
- end
- end
-
- def select_dropdown_option(option)
- find('.create-mr-dropdown-wrap .dropdown-toggle').click
- find("li[data-value='#{option}']").click
- find('.js-create-merge-request').click
- end
-end
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
new file mode 100644
index 00000000000..539d7e9ff01
--- /dev/null
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -0,0 +1,248 @@
+require 'rails_helper'
+
+describe 'User creates branch and merge request on issue page', :js do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
+
+ context 'when signed out' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it "doesn't show 'Create merge request' button" do
+ expect(page).not_to have_selector('.create-mr-dropdown-wrap')
+ end
+ end
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ context 'when interacting with the dropdown' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ # In order to improve tests performance, all UI checks are placed in this test.
+ it 'shows elements' do
+ button_create_merge_request = find('.js-create-merge-request')
+ button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
+
+ button_toggle_dropdown.click
+
+ dropdown = find('.create-merge-request-dropdown-menu')
+
+ page.within(dropdown) do
+ button_create_target = find('.js-create-target')
+ input_branch_name = find('.js-branch-name')
+ input_source = find('.js-ref')
+ li_create_branch = find("li[data-value='create-branch']")
+ li_create_merge_request = find("li[data-value='create-mr']")
+
+ # Test that all elements are presented.
+ expect(page).to have_content('Create merge request and branch')
+ expect(page).to have_content('Create branch')
+ expect(page).to have_content('Branch name')
+ expect(page).to have_content('Source (branch or tag)')
+ expect(page).to have_button('Create merge request')
+ expect(page).to have_selector('.js-branch-name:focus')
+
+ test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
+ test_branch_name_checking(input_branch_name)
+ test_source_checking(input_source)
+
+ # The button inside dropdown should be disabled if any errors occured.
+ expect(page).to have_button('Create branch', disabled: true)
+ end
+
+ # The top level button should be disabled if any errors occured.
+ expect(page).to have_button('Create branch', disabled: true)
+ end
+
+ context 'when branch name is auto-generated' do
+ it 'creates a merge request' do
+ perform_enqueued_jobs do
+ select_dropdown_option('create-mr')
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+
+ wait_for_requests
+ end
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_content('created branch 1-cherry-coloured-funk')
+ expect(page).to have_content('mentioned in merge request !1')
+ end
+
+ it 'creates a branch' do
+ select_dropdown_option('create-branch')
+
+ wait_for_requests
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
+ end
+ end
+
+ context 'when branch name is custom' do
+ let(:branch_name) { 'custom-branch-name' }
+
+ it 'creates a merge request' do
+ perform_enqueued_jobs do
+ select_dropdown_option('create-mr', branch_name)
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(page).to have_content('Request to merge custom-branch-name into')
+ expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+
+ wait_for_requests
+ end
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_content('created branch custom-branch-name')
+ expect(page).to have_content('mentioned in merge request !1')
+ end
+
+ it 'creates a branch' do
+ select_dropdown_option('create-branch', branch_name)
+
+ wait_for_requests
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
+ expect(current_path).to eq project_tree_path(project, branch_name)
+ end
+ end
+ end
+
+ context "when there is a referenced merge request" do
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}")
+ end
+
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes #{issue.to_reference}", author: user)
+ end
+
+ before do
+ referenced_mr.cache_merge_request_closes_issues!(user)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'disables the create branch button' do
+ expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+
+ context 'when merge requests are disabled' do
+ before do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows only create branch button' do
+ expect(page).not_to have_button('Create merge request')
+ expect(page).to have_button('Create branch')
+ end
+ end
+
+ context 'when issue is confidential' do
+ let(:issue) { create(:issue, :confidential, project: project) }
+
+ it 'disables the create branch button' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_css('.create-mr-dropdown-wrap')
+ end
+ end
+ end
+
+ private
+
+ def select_dropdown_option(option, branch_name = nil)
+ find('.create-mr-dropdown-wrap .dropdown-toggle').click
+ find("li[data-value='#{option}']").click
+
+ if branch_name
+ find('.js-branch-name').set(branch_name)
+
+ # Javascript debounces AJAX calls.
+ # So we have to wait until AJAX requests are started.
+ # Details are in app/assets/javascripts/create_merge_request_dropdown.js
+ # this.refDebounce = _.debounce(...)
+ sleep 0.5
+
+ wait_for_requests
+ end
+
+ find('.js-create-merge-request').click
+ end
+
+ def test_branch_name_checking(input_branch_name)
+ expect(input_branch_name.value).to eq(issue.to_branch_name)
+
+ input_branch_name.set('new-branch-name')
+ branch_name_message = find('.js-branch-message')
+
+ expect(branch_name_message).to have_text('Checking branch name availability…')
+
+ wait_for_requests
+
+ expect(branch_name_message).to have_text('Branch name is available')
+
+ input_branch_name.set(project.default_branch)
+
+ expect(branch_name_message).to have_text('Checking branch name availability…')
+
+ wait_for_requests
+
+ expect(branch_name_message).to have_text('Branch is already taken')
+ end
+
+ def test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
+ page.within(li_create_merge_request) do
+ expect(page).to have_css('i.fa.fa-check')
+ expect(button_create_target).to have_text('Create merge request')
+ expect(button_create_merge_request).to have_text('Create merge request')
+ end
+
+ li_create_branch.click
+
+ page.within(li_create_branch) do
+ expect(page).to have_css('i.fa.fa-check')
+ expect(button_create_target).to have_text('Create branch')
+ expect(button_create_merge_request).to have_text('Create branch')
+ end
+ end
+
+ def test_source_checking(input_source)
+ expect(input_source.value).to eq(project.default_branch)
+
+ input_source.set('mas') # Intentionally entered first 3 letters of `master` to check autocomplete feature later.
+ source_message = find('.js-ref-message')
+
+ expect(source_message).to have_text('Checking source availability…')
+
+ wait_for_requests
+
+ expect(source_message).to have_text('Source is not available')
+
+ # JavaScript gets refs started with `mas` (entered above) and places the first match.
+ # User sees `mas` in black color (the part he entered) and the `ter` in gray color (a hint).
+ # Since hinting is implemented via text selection and rspec/capybara doesn't have matchers for it,
+ # we just checking the whole source name.
+ expect(input_source.value).to eq(project.default_branch)
+ end
+end
diff --git a/spec/features/logout_spec.rb b/spec/features/logout_spec.rb
new file mode 100644
index 00000000000..635729efa53
--- /dev/null
+++ b/spec/features/logout_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'Logout/Sign out', :js do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ it 'sign out redirects to sign in page' do
+ gitlab_sign_out
+
+ expect(current_path).to eq new_user_session_path
+ end
+
+ it 'sign out does not show signed out flash notice' do
+ gitlab_sign_out
+
+ expect(page).not_to have_selector('.flash-notice')
+ end
+end
diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb
new file mode 100644
index 00000000000..01f9526608f
--- /dev/null
+++ b/spec/features/projects/clusters/interchangeability_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do
+ EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze
+ EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze
+
+ it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do
+ expected_interfaces = KubernetesService.instance_methods(false)
+ expected_interfaces = expected_interfaces - EXCEPT_METHODS
+ EXCEPT_METHODS_GREP_V.each do |g|
+ expected_interfaces = expected_interfaces.grep_v(/#{Regexp.escape(g)}\z/)
+ end
+
+ expect(expected_interfaces - Clusters::Platforms::Kubernetes.instance_methods).to be_empty
+ end
+end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 5fc3ba54f65..dfcf97ad495 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -101,35 +101,48 @@ feature 'Environment' do
end
context 'with terminal' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
- scenario 'it shows the terminal button' do
- expect(page).to have_terminal_button
+ context 'web terminal', :js do
+ before do
+ # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
+ allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ visit terminal_project_environment_path(project, environment)
+ end
+
+ it 'displays a web terminal' do
+ expect(page).to have_selector('#terminal')
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
- context 'web terminal', :js do
- before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
- visit terminal_project_environment_path(project, environment)
- end
+ context 'for developer' do
+ let(:role) { :developer }
- it 'displays a web terminal' do
- expect(page).to have_selector('#terminal')
- expect(page).to have_link(nil, href: environment.external_url)
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
end
end
end
- context 'for developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 879ee6f4b9b..4a05313c14a 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -208,22 +208,35 @@ feature 'Environments page', :js do
end
context 'when kubernetes terminal is available' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ it 'shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'when user is a developer' do
+ let(:role) { :developer }
- it 'shows the terminal button' do
- expect(page).to have_terminal_button
+ it 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
end
end
- context 'when user is a developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- it 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [create(:project, :repository)]) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index e10d29e5eea..842840cc04c 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Project fork' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -24,8 +26,9 @@ describe 'Project fork' do
end
context 'master in group' do
+ let(:group) { create(:group) }
+
before do
- group = create(:group)
group.add_master(user)
end
@@ -53,5 +56,17 @@ describe 'Project fork' do
expect(page).to have_css('.fork-thumbnail', count: 2)
expect(page).to have_css('.fork-thumbnail.disabled')
end
+
+ it 'links to the fork if the project was already forked within that namespace' do
+ forked_project = fork_project(project, user, namespace: group, repository: true)
+
+ visit new_project_fork_path(project)
+
+ expect(page).to have_css('div.forked', text: group.full_name)
+
+ click_link group.full_name
+
+ expect(current_path).to eq(project_path(forked_project))
+ end
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index fb6a3b8e733..0c354298433 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 1686e7fa342..156293289dd 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -26,9 +26,11 @@ feature 'Multi-file editor new directory', :js do
click_button('Create directory')
end
+ find('.multi-file-commit-panel-collapse-btn').click
+
fill_in('commit-message', with: 'commit message')
- click_button('Commit 1 file')
+ click_button('Commit')
expect(page).to have_selector('td', text: 'commit message')
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 1e2de0711b8..8fb8476e631 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -26,9 +26,11 @@ feature 'Multi-file editor new file', :js do
click_button('Create file')
end
+ find('.multi-file-commit-panel-collapse-btn').click
+
fill_in('commit-message', with: 'commit message')
- click_button('Commit 1 file')
+ click_button('Commit')
expect(page).to have_selector('td', text: 'commit message')
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index 8439bb5a69e..d4e57d1ecfa 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -26,7 +26,7 @@ feature 'Multi-file editor upload file', :js do
find('.add-to-tree').click
- expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt')
+ expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
end
@@ -39,7 +39,7 @@ feature 'Multi-file editor upload file', :js do
find('.add-to-tree').click
- expect(page).to have_selector('.repo-tab', text: 'dk.png')
+ expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor')
expect(page).to have_content('The source could not be displayed for this temporary file.')
end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index e5158761333..d2c7867febb 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -26,9 +26,10 @@ describe ButtonHelper do
context 'when user has password automatically set' do
let(:user) { create(:user, password_automatically_set: true) }
- it 'shows a password tooltip' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Set a password on your account to pull or push via HTTP.')
+ it 'shows the password text on the dropdown' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq 'Set a password on your account to pull or push via HTTP.'
end
end
end
@@ -39,17 +40,10 @@ describe ButtonHelper do
end
context 'when user has no personal access tokens' do
- it 'has a personal access token tooltip ' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Create a personal access token on your account to pull or push via HTTP.')
- end
- end
-
- context 'when user has a personal access token' do
- it 'shows no tooltip' do
- create(:personal_access_token, user: user)
+ it 'has a personal access token text on the dropdown description ' do
+ description = element.search('.dropdown-menu-inner-content').first
- expect(element.attr('class')).not_to include(has_tooltip_class)
+ expect(description.inner_text).to eq 'Create a personal access token on your account to pull or push via HTTP.'
end
end
end
@@ -63,6 +57,41 @@ describe ButtonHelper do
end
end
+ describe 'ssh_button' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def element
+ element = helper.ssh_clone_button(project)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'without an ssh key on the user' do
+ it 'shows a warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+ end
+ end
+
+ context 'with an ssh key on the user' do
+ before do
+ create(:key, user: user)
+ end
+
+ it 'there is no warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description).to eq nil
+ end
+ end
+ end
+
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 67afba19190..960b731892a 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,21 +1,18 @@
-/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
-
import '~/behaviors/autosize';
-(function() {
- describe('Autosize behavior', function() {
- var load;
- beforeEach(function() {
- return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
- });
- it('does not overwrite the resize property', function() {
- load();
- return expect($('textarea')).toHaveCss({
- resize: 'vertical'
- });
+function load() {
+ $(document).trigger('load');
+}
+
+describe('Autosize behavior', () => {
+ beforeEach(() => {
+ setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ });
+
+ it('does not overwrite the resize property', () => {
+ load();
+ expect($('textarea')).toHaveCss({
+ resize: 'vertical',
});
- return load = function() {
- return $(document).trigger('load');
- };
});
-}).call(window);
+});
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index f9fa814b801..8287c58ac5a 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,39 +1,43 @@
-/* eslint-disable space-before-function-paren, no-var */
-
import '~/behaviors/requires_input';
-(function() {
- describe('requiresInput', function() {
- preloadFixtures('branches/new_branch.html.raw');
- beforeEach(function() {
- loadFixtures('branches/new_branch.html.raw');
- this.submitButton = $('button[type="submit"]');
- });
- it('disables submit when any field is required', function() {
- $('.js-requires-input').requiresInput();
- return expect(this.submitButton).toBeDisabled();
- });
- it('enables submit when no field is required', function() {
- $('*[required=required]').removeAttr('required');
- $('.js-requires-input').requiresInput();
- return expect(this.submitButton).not.toBeDisabled();
- });
- it('enables submit when all required fields are pre-filled', function() {
- $('*[required=required]').remove();
- $('.js-requires-input').requiresInput();
- return expect($('.submit')).not.toBeDisabled();
- });
- it('enables submit when all required fields receive input', function() {
- $('.js-requires-input').requiresInput();
- $('#required1').val('input1').change();
- expect(this.submitButton).toBeDisabled();
- $('#optional1').val('input1').change();
- expect(this.submitButton).toBeDisabled();
- $('#required2').val('input2').change();
- $('#required3').val('input3').change();
- $('#required4').val('input4').change();
- $('#required5').val('1').change();
- return expect($('.submit')).not.toBeDisabled();
- });
+describe('requiresInput', () => {
+ let submitButton;
+ preloadFixtures('branches/new_branch.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('branches/new_branch.html.raw');
+ submitButton = $('button[type="submit"]');
+ });
+
+ it('disables submit when any field is required', () => {
+ $('.js-requires-input').requiresInput();
+ expect(submitButton).toBeDisabled();
+ });
+
+ it('enables submit when no field is required', () => {
+ $('*[required=required]').removeAttr('required');
+ $('.js-requires-input').requiresInput();
+ expect(submitButton).not.toBeDisabled();
+ });
+
+ it('enables submit when all required fields are pre-filled', () => {
+ $('*[required=required]').remove();
+ $('.js-requires-input').requiresInput();
+ expect($('.submit')).not.toBeDisabled();
+ });
+
+ it('enables submit when all required fields receive input', () => {
+ $('.js-requires-input').requiresInput();
+ $('#required1').val('input1').change();
+ expect(submitButton).toBeDisabled();
+
+ $('#optional1').val('input1').change();
+ expect(submitButton).toBeDisabled();
+
+ $('#required2').val('input2').change();
+ $('#required3').val('input3').change();
+ $('#required4').val('input4').change();
+ $('#required5').val('1').change();
+ expect($('.submit')).not.toBeDisabled();
});
-}).call(window);
+});
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 1ef494a00b8..1225fe2cb66 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -279,7 +279,12 @@ describe('DropDown', function () {
describe('addEvents', function () {
beforeEach(function () {
this.list = { addEventListener: () => {} };
- this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} };
+ this.dropdown = {
+ list: this.list,
+ clickEvent: () => {},
+ closeDropdown: () => {},
+ eventWrapper: {},
+ };
spyOn(this.list, 'addEventListener');
@@ -288,6 +293,7 @@ describe('DropDown', function () {
it('should call .addEventListener', function () {
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
+ expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
});
});
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 75bf5f3d611..3d39bd0812b 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -24,7 +24,7 @@ describe('Hook', function () {
});
it('should call DropDown constructor', function () {
- expect(dropdownSrc.default).toHaveBeenCalledWith(this.list);
+ expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function () {
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index db75262b562..04a7f8e32f1 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import Autosize from 'autosize';
import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue';
-import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
+import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => {
@@ -23,7 +23,7 @@ describe('issue_comment_form component', () => {
describe('user is logged in', () => {
beforeEach(() => {
store.dispatch('setUserData', userDataMock);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = mountComponent();
@@ -178,7 +178,7 @@ describe('issue_comment_form component', () => {
describe('issue is confidential', () => {
it('shows information warning', (done) => {
- store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true }));
+ store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
Vue.nextTick(() => {
expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
done();
@@ -190,7 +190,7 @@ describe('issue_comment_form component', () => {
describe('user is not logged in', () => {
beforeEach(() => {
store.dispatch('setUserData', null);
- store.dispatch('setIssueData', loggedOutIssueData);
+ store.dispatch('setNoteableData', loggedOutnoteableData);
store.dispatch('setNotesData', notesDataMock);
vm = mountComponent();
diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js
index 05c6b57f93e..b6ae55d44f5 100644
--- a/spec/javascripts/notes/components/issue_discussion_spec.js
+++ b/spec/javascripts/notes/components/issue_discussion_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
import issueDiscussion from '~/notes/components/issue_discussion.vue';
-import { issueDataMock, discussionMock, notesDataMock } from '../mock_data';
+import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
describe('issue_discussion component', () => {
let vm;
@@ -9,7 +9,7 @@ describe('issue_discussion component', () => {
beforeEach(() => {
const Component = Vue.extend(issueDiscussion);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js
index 22e91c4c40f..8e43037f356 100644
--- a/spec/javascripts/notes/components/issue_note_app_spec.js
+++ b/spec/javascripts/notes/components/issue_note_app_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueNotesApp from '~/notes/components/issue_notes_app.vue';
-import service from '~/notes/services/issue_notes_service';
+import service from '~/notes/services/notes_service';
import * as mockData from '../mock_data';
describe('issue_note_app', () => {
@@ -24,7 +24,7 @@ describe('issue_note_app', () => {
mountComponent = (data) => {
const props = data || {
- issueData: mockData.issueDataMock,
+ noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
userData: mockData.userDataMock,
};
@@ -60,7 +60,7 @@ describe('issue_note_app', () => {
});
it('should set issue data', () => {
- expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock);
+ expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock);
});
it('should set user data', () => {
diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js
index 81f07ed47cc..37aad50737b 100644
--- a/spec/javascripts/notes/components/issue_note_body_spec.js
+++ b/spec/javascripts/notes/components/issue_note_body_spec.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
import noteBody from '~/notes/components/issue_note_body.vue';
-import { issueDataMock, notesDataMock, note } from '../mock_data';
+import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
let vm;
@@ -10,7 +10,7 @@ describe('issue_note_body component', () => {
beforeEach(() => {
const Component = Vue.extend(noteBody);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js
index a90dbcb72b5..d42ef239711 100644
--- a/spec/javascripts/notes/components/issue_note_form_spec.js
+++ b/spec/javascripts/notes/components/issue_note_form_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
import issueNoteForm from '~/notes/components/issue_note_form.vue';
-import { issueDataMock, notesDataMock } from '../mock_data';
+import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => {
@@ -11,7 +11,7 @@ describe('issue_note_form component', () => {
beforeEach(() => {
const Component = Vue.extend(issueNoteForm);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
props = {
diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js
index 7ef85d5b4f0..73fd188dbe5 100644
--- a/spec/javascripts/notes/components/issue_note_spec.js
+++ b/spec/javascripts/notes/components/issue_note_spec.js
@@ -2,7 +2,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
import issueNote from '~/notes/components/issue_note.vue';
-import { issueDataMock, notesDataMock, note } from '../mock_data';
+import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
let vm;
@@ -10,7 +10,7 @@ describe('issue_note', () => {
beforeEach(() => {
const Component = Vue.extend(issueNote);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js
index 7292e5c3a8d..15995ec5a05 100644
--- a/spec/javascripts/notes/components/note_awards_list_spec.js
+++ b/spec/javascripts/notes/components/note_awards_list_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
-import { issueDataMock, notesDataMock } from '../mock_data';
+import { noteableDataMock, notesDataMock } from '../mock_data';
describe('note_awards_list component', () => {
let vm;
@@ -10,7 +10,7 @@ describe('note_awards_list component', () => {
beforeEach(() => {
const Component = Vue.extend(awardsNote);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
awardsMock = [
{
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 89ba3a002b7..42497de3c55 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -18,7 +18,7 @@ export const userDataMock = {
username: 'root',
};
-export const issueDataMock = {
+export const noteableDataMock = {
assignees: [],
author_id: 1,
branch_name: null,
@@ -271,7 +271,7 @@ export const discussionMock = {
individual_note: false,
};
-export const loggedOutIssueData = {
+export const loggedOutnoteableData = {
"id": 98,
"iid": 26,
"author_id": 1,
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 3d1ca870ca4..e092320f9a3 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,6 +1,6 @@
import * as actions from '~/notes/stores/actions';
import testAction from '../../helpers/vuex_action_helper';
-import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
describe('setNotesData', () => {
@@ -11,10 +11,10 @@ describe('Actions Notes Store', () => {
});
});
- describe('setIssueData', () => {
+ describe('setNoteableData', () => {
it('should set received issue data', (done) => {
- testAction(actions.setIssueData, null, { issueData: {} }, [
- { type: 'SET_ISSUE_DATA', payload: issueDataMock },
+ testAction(actions.setNoteableData, null, { noteableData: {} }, [
+ { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock },
], done);
});
});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 48ee1bf9a52..c5a84b71788 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -1,5 +1,5 @@
import * as getters from '~/notes/stores/getters';
-import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Getters Notes Store', () => {
let state;
@@ -11,7 +11,7 @@ describe('Getters Notes Store', () => {
notesData: notesDataMock,
userData: userDataMock,
- issueData: issueDataMock,
+ noteableData: noteableDataMock,
};
});
describe('notes', () => {
@@ -32,9 +32,9 @@ describe('Getters Notes Store', () => {
});
});
- describe('getIssueData', () => {
- it('should return all data in `issueData`', () => {
- expect(getters.getIssueData(state)).toEqual(issueDataMock);
+ describe('getNoteableData', () => {
+ it('should return all data in `noteableData`', () => {
+ expect(getters.getNoteableData(state)).toEqual(noteableDataMock);
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 1e22e03e178..22d99998a7d 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,5 +1,5 @@
import mutations from '~/notes/stores/mutations';
-import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Mutation Notes Store', () => {
describe('ADD_NEW_NOTE', () => {
@@ -74,14 +74,14 @@ describe('Mutation Notes Store', () => {
});
});
- describe('SET_ISSUE_DATA', () => {
+ describe('SET_NOTEABLE_DATA', () => {
it('should set the issue data', () => {
const state = {
- issueData: {},
+ noteableData: {},
};
- mutations.SET_ISSUE_DATA(state, issueDataMock);
- expect(state.issueData).toEqual(issueDataMock);
+ mutations.SET_NOTEABLE_DATA(state, noteableDataMock);
+ expect(state.noteableData).toEqual(noteableDataMock);
});
});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
index 171629fcd6b..edef150dd1e 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
@@ -50,6 +50,18 @@ describe('ProjectsListItemComponent', () => {
expect(vm.highlightedProjectName).toBe(mockProject.name);
});
});
+
+ describe('truncatedNamespace', () => {
+ it('should truncate project name from namespace string', () => {
+ vm.namespace = 'platform / nokia-3310';
+ expect(vm.truncatedNamespace).toBe('platform');
+ });
+
+ it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
+ vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310';
+ expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset');
+ });
+ });
});
describe('template', () => {
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
new file mode 100644
index 00000000000..f750061a6a1
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list collapsed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listCollapsed);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.openFiles.push(file(), file());
+ vm.$store.state.openFiles[0].tempFile = true;
+ vm.$store.state.openFiles.forEach((f) => {
+ Object.assign(f, {
+ changed: true,
+ });
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders added & modified files count', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ });
+});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
new file mode 100644
index 00000000000..18c9b46fcd9
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import listItem from '~/repo/components/commit_sidebar/list_item.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list item', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listItem);
+
+ f = file();
+
+ vm = mountComponent(Component, {
+ file: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file path', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
+ });
+
+ describe('computed', () => {
+ describe('iconName', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconName).toBe('file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconName).toBe('file-addition');
+ });
+ });
+
+ describe('iconClass', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconClass).toContain('multi-file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconClass).toContain('multi-file-addition');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
new file mode 100644
index 00000000000..df7e3c5de21
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import commitSidebarList from '~/repo/components/commit_sidebar/list.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(commitSidebarList);
+
+ vm = createComponentWithStore(Component, store, {
+ title: 'Staged',
+ fileList: [],
+ collapsed: false,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('empty file list', () => {
+ it('renders no changes text', () => {
+ expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes');
+ });
+ });
+
+ describe('with a list of files', () => {
+ beforeEach((done) => {
+ const f = file('file name');
+ f.changed = true;
+ vm.fileList.push(f);
+
+ Vue.nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.collapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+
+ it('hides list', () => {
+ expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
+ expect(vm.$el.querySelector('.help-block')).toBeNull();
+ });
+
+ it('hides collapse button', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull();
+ });
+ });
+
+ it('clicking toggle collapse button emits toggle event', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed');
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index 0f991e1b727..1c794123095 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -25,8 +25,12 @@ describe('RepoCommitSection', () => {
return comp.$mount();
}
- beforeEach(() => {
+ beforeEach((done) => {
vm = createComponent();
+
+ vm.collapsed = false;
+
+ Vue.nextTick(done);
});
afterEach(() => {
@@ -36,12 +40,11 @@ describe('RepoCommitSection', () => {
});
it('renders a commit section', () => {
- const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
- const submitCommit = vm.$el.querySelector('.btn');
- const targetBranch = vm.$el.querySelector('.target-branch');
+ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
+ const submitCommit = vm.$el.querySelector('form .btn');
- expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
- expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)');
+ expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
+ expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged');
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
@@ -49,10 +52,7 @@ describe('RepoCommitSection', () => {
});
expect(submitCommit.disabled).toBeTruthy();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
- expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
- expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual('master');
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
describe('when submitting', () => {
@@ -69,7 +69,7 @@ describe('RepoCommitSection', () => {
});
it('allows you to submit', () => {
- expect(vm.$el.querySelector('.btn').disabled).toBeTruthy();
+ expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy();
});
it('submits commit', (done) => {
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 979d2185076..81158cad639 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
import store from '~/repo/stores';
import repoEditor from '~/repo/components/repo_editor.vue';
+import monacoLoader from '~/repo/monaco_loader';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
let vm;
- beforeEach(() => {
+ beforeEach((done) => {
const f = file();
const RepoEditor = Vue.extend(repoEditor);
@@ -21,6 +22,10 @@ describe('RepoEditor', () => {
vm.monaco = true;
vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
});
afterEach(() => {
@@ -32,7 +37,6 @@ describe('RepoEditor', () => {
it('renders an ide container', (done) => {
Vue.nextTick(() => {
expect(vm.shouldHideEditor).toBeFalsy();
- expect(vm.$el.textContent.trim()).toBe('');
done();
});
@@ -50,7 +54,7 @@ describe('RepoEditor', () => {
});
it('shows activeFile html', () => {
- expect(vm.$el.textContent.trim()).toBe('testing');
+ expect(vm.$el.textContent).toContain('testing');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 7cb4dace491..df7cf8aabbb 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -29,7 +29,6 @@ describe('RepoSidebar', () => {
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
- expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
@@ -40,18 +39,6 @@ describe('RepoSidebar', () => {
expect(tbody.querySelector('.file')).toBeTruthy();
});
- it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', (done) => {
- vm.$store.state.openFiles.push(vm.$store.state.tree[0]);
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
- expect(vm.$el.querySelector('thead')).toBeTruthy();
- expect(vm.$el.querySelector('thead .repo-file-options')).toBeTruthy();
-
- done();
- });
- });
-
it('renders 5 loading files if tree is loading', (done) => {
vm.$store.state.tree = [];
vm.$store.state.loading = true;
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index df0ca55aafc..7d2174196c9 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -24,8 +24,8 @@ describe('RepoTab', () => {
tab: file(),
});
vm.$store.state.openFiles.push(vm.tab);
- const close = vm.$el.querySelector('.close-btn');
- const name = vm.$el.querySelector(`a[title="${vm.tab.url}"]`);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
expect(close.querySelector('.fa-times')).toBeTruthy();
expect(name.textContent.trim()).toEqual(vm.tab.name);
@@ -50,7 +50,7 @@ describe('RepoTab', () => {
spyOn(vm, 'closeFile');
- vm.$el.querySelector('.close-btn').click();
+ vm.$el.querySelector('.multi-file-tab-close').click();
expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
});
@@ -62,7 +62,7 @@ describe('RepoTab', () => {
tab,
});
- expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy();
+ expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull();
});
describe('methods', () => {
@@ -77,7 +77,7 @@ describe('RepoTab', () => {
vm.$store.state.openFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
- vm.$el.querySelector('.close-btn').click();
+ vm.$el.querySelector('.multi-file-tab-close').click();
vm.$nextTick(() => {
expect(tab.opened).toBeTruthy();
@@ -95,7 +95,7 @@ describe('RepoTab', () => {
vm.$store.state.openFiles.push(tab);
vm.$store.dispatch('setFileActive', tab);
- vm.$el.querySelector('.close-btn').click();
+ vm.$el.querySelector('.multi-file-tab-close').click();
vm.$nextTick(() => {
expect(tab.opened).toBeFalsy();
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index d0246cc72e6..1fb2242c051 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -25,12 +25,11 @@ describe('RepoTabs', () => {
vm.$store.state.openFiles = openedFiles;
vm.$nextTick(() => {
- const tabs = [...vm.$el.querySelectorAll(':scope > li')];
+ const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
- expect(tabs.length).toEqual(3);
+ expect(tabs.length).toEqual(2);
expect(tabs[0].classList.contains('active')).toBeTruthy();
expect(tabs[1].classList.contains('active')).toBeFalsy();
- expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
done();
});
diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..62c3913bf4d
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/repo/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..8c134f178c0
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_manager_spec.js
@@ -0,0 +1,81 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import ModelManager from '~/repo/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
new file mode 100644
index 00000000000..d41ade237ca
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_spec.js
@@ -0,0 +1,84 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', (done) => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..2e32e8fa0bd
--- /dev/null
+++ b/spec/javascripts/repo/lib/decorations/controller_spec.js
@@ -0,0 +1,120 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..ed62e28d3a3
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/controller_spec.js
@@ -0,0 +1,176 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import ModelManager from '~/repo/lib/common/model_manager';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller';
+import { computeDiff } from '~/repo/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file());
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(
+ getDecorator(change).options.linesDecorationsClassName,
+ ).toBe(`dirty-diff dirty-diff-${type}`);
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
+
+ controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } });
+
+ expect(spy).toHaveBeenCalledWith([], [{
+ range: new monaco.Range(
+ 1, 1, 1, 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ }]);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..3269ec5d2c9
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/repo/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
new file mode 100644
index 00000000000..b4887d063ed
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_options_spec.js
@@ -0,0 +1,7 @@
+import editorOptions from '~/repo/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
new file mode 100644
index 00000000000..cd32832a232
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_spec.js
@@ -0,0 +1,128 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ describe('createInstance', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(el);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(el);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
index a204b2386cd..952b8ec3a59 100644
--- a/spec/javascripts/repo/stores/getters_spec.js
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -116,4 +116,31 @@ describe('Multi-file store getters', () => {
expect(getters.canEditFile(localState)).toBeFalsy();
});
});
+
+ describe('modifiedFiles', () => {
+ it('returns a list of modified files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('changed'));
+ localState.openFiles[1].changed = true;
+
+ const modifiedFiles = getters.modifiedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('addedFiles', () => {
+ it('returns a list of added files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('added'));
+ localState.openFiles[1].changed = true;
+ localState.openFiles[1].tempFile = true;
+
+ const modifiedFiles = getters.addedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('added');
+ });
+ });
});
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index 4d3fdbd9554..84d9e635810 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -1,9 +1,13 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate do
+describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do
+ let(:merge_request_diffs) { table(:merge_request_diffs) }
+ let(:merge_requests) { table(:merge_requests) }
+
describe '#perform' do
- let(:merge_request) { create(:merge_request) }
- let(:merge_request_diff) { merge_request.merge_request_diff }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { merge_requests.create!(iid: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master').becomes(MergeRequest) }
+ let(:merge_request_diff) { MergeRequest.find(merge_request.id).create_merge_request_diff }
let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) }
def diffs_to_hashes(diffs)
@@ -68,7 +72,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
let(:stop_id) { described_class::MergeRequestDiff.maximum(:id) }
before do
- merge_request.reload_diff(true)
+ merge_request.create_merge_request_diff
convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files))
convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files))
@@ -288,7 +292,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs are Rugged::Patch instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
- let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
let(:expected_commits) { commits }
let(:diffs) { first_commit.rugged_diff_from_parent.patches }
let(:expected_diffs) { [] }
@@ -298,7 +302,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs are Rugged::Diff::Delta instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
- let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
let(:expected_commits) { commits }
let(:diffs) { first_commit.rugged_diff_from_parent.deltas }
let(:expected_diffs) { [] }
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index 994992f79d4..e52baf8dde7 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -62,12 +62,15 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
expect(base2_membership).not_to be_nil
end
- it 'skips links that had their source project deleted' do
- forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: create(:project).id)
+ it 'creates a fork network for the fork of which the source was deleted' do
+ fork = create(:project)
+ forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id)
migration.perform(5, 8)
expect(fork_networks.find_by(root_project_id: 99999)).to be_nil
+ expect(fork_networks.find_by(root_project_id: fork.id)).not_to be_nil
+ expect(fork_network_members.find_by(project_id: fork.id)).not_to be_nil
end
it 'schedules a job for inserting memberships for forks-of-forks' do
diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
index 15eb01eb472..4884d5f8ba4 100644
--- a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
@@ -4,11 +4,24 @@ describe Gitlab::Ci::Build::Policy::Kubernetes do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when kubernetes service is active' do
- set(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'is satisfied by a kubernetes pipeline' do
+ expect(described_class.new('active'))
+ .to be_satisfied_by(pipeline)
+ end
+ end
- it 'is satisfied by a kubernetes pipeline' do
- expect(described_class.new('active'))
- .to be_satisfied_by(pipeline)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d72f8553f55..98880fe9f28 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -178,15 +178,29 @@ module Gitlab
end
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
+ expect(seeds.size).to eq 2
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index f7c90093bde..f0752649121 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -3146,13 +3146,12 @@
"merge_request_diff": {
"id": 26,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "merge_request_diff_id": 26,
+ "sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "relative_order": 0,
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:26:01.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3161,9 +3160,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
+ "merge_request_diff_id": 26,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
"new_path": "files/ruby/feature.rb",
"old_path": "files/ruby/feature.rb",
"a_mode": "0",
@@ -3425,13 +3426,12 @@
"merge_request_diff": {
"id": 15,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
+ "merge_request_diff_id": 15,
+ "relative_order": 0,
+ "sha": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T13:22:56.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -3440,9 +3440,11 @@
"committer_email": "james@jameslopez.es"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 15,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -3704,13 +3706,12 @@
"merge_request_diff": {
"id": 14,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "ddd4ff416a931589c695eb4f5b23f844426f6928",
+ "merge_request_diff_id": 14,
+ "relative_order": 0,
+ "sha": "ddd4ff416a931589c695eb4f5b23f844426f6928",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:14:43.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -3719,12 +3720,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 14,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -3733,11 +3732,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 14,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -3746,11 +3744,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 14,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3759,12 +3756,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 14,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3773,11 +3768,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 14,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3786,12 +3780,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 14,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3800,11 +3792,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 14,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -3813,11 +3804,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 14,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -3826,12 +3816,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 14,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3840,11 +3828,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 14,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -3853,11 +3840,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 14,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -3866,11 +3852,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 14,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -3879,12 +3864,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 14,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -3893,11 +3876,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 14,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -3906,11 +3888,10 @@
"committer_email": "marmis85@gmail.com"
},
{
- "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "merge_request_diff_id": 14,
+ "relative_order": 15,
+ "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
- ],
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3919,11 +3900,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "merge_request_diff_id": 14,
+ "relative_order": 16,
+ "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- ],
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3932,11 +3912,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "merge_request_diff_id": 14,
+ "relative_order": 17,
+ "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "d14d6c0abdd253381df51a723d58691b2ee1ab08"
- ],
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3945,11 +3924,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "merge_request_diff_id": 14,
+ "relative_order": 18,
+ "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
- ],
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3958,11 +3936,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "merge_request_diff_id": 14,
+ "relative_order": 19,
+ "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3971,9 +3948,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 0,
+ "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
"old_path": ".DS_Store",
"a_mode": "100644",
@@ -3984,7 +3963,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 1,
+ "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
"new_path": ".gitignore",
"old_path": ".gitignore",
"a_mode": "100644",
@@ -3995,7 +3976,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 2,
+ "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
"new_path": ".gitmodules",
"old_path": ".gitmodules",
"a_mode": "100644",
@@ -4006,7 +3989,9 @@
"too_large": false
},
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 3,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -4017,7 +4002,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -4028,7 +4015,9 @@
"too_large": false
},
{
- "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 5,
+ "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
"new_path": "files/.DS_Store",
"old_path": "files/.DS_Store",
"a_mode": "100644",
@@ -4039,7 +4028,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4050,7 +4041,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 7,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -4061,7 +4054,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 8,
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -4072,7 +4067,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 9,
+ "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
"new_path": "files/ruby/regex.rb",
"old_path": "files/ruby/regex.rb",
"a_mode": "100644",
@@ -4083,7 +4080,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 14,
+ "relative_order": 10,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -4094,7 +4093,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 11,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -4105,7 +4106,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 12,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
"new_path": "gitlab-grack",
"old_path": "gitlab-grack",
"a_mode": "0",
@@ -4116,7 +4119,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 13,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
"new_path": "gitlab-shell",
"old_path": "gitlab-shell",
"a_mode": "0",
@@ -4127,7 +4132,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 14,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -4215,6 +4222,7 @@
},
"events": [
{
+ "merge_request_diff_id": 14,
"id": 529,
"target_type": "Note",
"target_id": 793,
@@ -4398,13 +4406,12 @@
"merge_request_diff": {
"id": 13,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "0bfedc29d30280c7e8564e19f654584b459e5868",
+ "merge_request_diff_id": 13,
+ "relative_order": 0,
+ "sha": "0bfedc29d30280c7e8564e19f654584b459e5868",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T15:25:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -4413,12 +4420,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 13,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -4427,11 +4432,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 13,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -4440,11 +4444,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 13,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4453,12 +4456,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 13,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4467,11 +4468,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 13,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4480,12 +4480,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 13,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4494,11 +4492,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 13,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4507,11 +4504,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 13,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4520,12 +4516,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 13,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4534,11 +4528,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 13,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4547,11 +4540,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 13,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -4560,11 +4552,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 13,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -4573,12 +4564,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 13,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -4587,11 +4576,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 13,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -4600,9 +4588,11 @@
"committer_email": "marmis85@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 0,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -4613,7 +4603,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 1,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -4624,7 +4616,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 2,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4635,7 +4629,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 3,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -4646,7 +4642,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 13,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -4657,7 +4655,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 5,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -4668,7 +4668,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -4930,13 +4932,12 @@
"merge_request_diff": {
"id": 12,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
+ "merge_request_diff_id": 12,
+ "relative_order": 0,
+ "sha": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:08:21.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -4945,12 +4946,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 12,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -4959,11 +4958,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 12,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -4972,11 +4970,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 12,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4985,12 +4982,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 12,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4999,11 +4994,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 12,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5012,12 +5006,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 12,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5026,11 +5018,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 12,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5039,11 +5030,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 12,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5052,12 +5042,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 12,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5066,11 +5054,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 12,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5079,11 +5066,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 12,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5092,11 +5078,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 12,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -5105,9 +5090,11 @@
"committer_email": "stanhu@packetzoom.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 0,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -5118,7 +5105,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 1,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -5129,7 +5118,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 2,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -5140,7 +5131,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 3,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -5151,7 +5144,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 12,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -5162,7 +5157,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 5,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -5424,9 +5421,9 @@
"merge_request_diff": {
"id": 11,
"state": "empty",
- "st_commits": null,
- "utf8_st_diffs": [
-
+ "merge_request_diff_commits": [
+ ],
+ "merge_request_diff_files": [
],
"merge_request_id": 11,
"created_at": "2016-06-14T15:02:23.772Z",
@@ -5679,13 +5676,12 @@
"merge_request_diff": {
"id": 10,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
+ "merge_request_diff_id": 10,
+ "relative_order": 0,
+ "sha": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:43:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -5694,12 +5690,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 10,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -5708,11 +5702,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 10,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -5721,11 +5714,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 10,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5734,12 +5726,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 10,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5748,11 +5738,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 10,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5761,12 +5750,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 10,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5775,11 +5762,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 10,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5788,11 +5774,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 10,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5801,12 +5786,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 10,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5815,11 +5798,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 10,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5828,11 +5810,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 10,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
@@ -5841,11 +5822,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 10,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -5854,12 +5834,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 10,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -5868,11 +5846,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 10,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -5881,11 +5858,10 @@
"committer_email": "marmis85@gmail.com"
},
{
- "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "merge_request_diff_id": 10,
+ "relative_order": 16,
+ "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
- ],
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5894,11 +5870,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "merge_request_diff_id": 10,
+ "relative_order": 17,
+ "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- ],
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5907,11 +5882,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "merge_request_diff_id": 10,
+ "relative_order": 18,
+ "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "d14d6c0abdd253381df51a723d58691b2ee1ab08"
- ],
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5920,11 +5894,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "merge_request_diff_id": 10,
+ "relative_order": 19,
+ "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
- ],
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5933,11 +5906,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "merge_request_diff_id": 10,
+ "relative_order": 20,
+ "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5946,9 +5918,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 0,
+ "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
"old_path": ".DS_Store",
"a_mode": "100644",
@@ -5959,7 +5933,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 1,
+ "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
"new_path": ".gitignore",
"old_path": ".gitignore",
"a_mode": "100644",
@@ -5970,7 +5946,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 2,
+ "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
"new_path": ".gitmodules",
"old_path": ".gitmodules",
"a_mode": "100644",
@@ -5981,7 +5959,9 @@
"too_large": false
},
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 3,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -5992,7 +5972,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -6003,7 +5985,9 @@
"too_large": false
},
{
- "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 5,
+ "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
"new_path": "files/.DS_Store",
"old_path": "files/.DS_Store",
"a_mode": "100644",
@@ -6014,7 +5998,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -6025,7 +6011,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 7,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -6036,7 +6024,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 8,
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -6047,7 +6037,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 9,
+ "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
"new_path": "files/ruby/regex.rb",
"old_path": "files/ruby/regex.rb",
"a_mode": "100644",
@@ -6058,7 +6050,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 10,
+ "relative_order": 10,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -6069,7 +6063,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 11,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -6080,7 +6076,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 12,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
"new_path": "gitlab-grack",
"old_path": "gitlab-grack",
"a_mode": "0",
@@ -6091,7 +6089,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 13,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
"new_path": "gitlab-shell",
"old_path": "gitlab-shell",
"a_mode": "0",
@@ -6102,7 +6102,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 14,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -6364,13 +6366,12 @@
"merge_request_diff": {
"id": 9,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
+ "merge_request_diff_id": 9,
+ "relative_order": 0,
+ "sha": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T15:44:02.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -6379,9 +6380,11 @@
"committer_email": "james@jameslopez.es"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 9,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4d78a4b9b13..0ab3afd0074 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -95,26 +95,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- it 'has the correct data for merge request st_diffs' do
- # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
- # one MergeRequestDiff uses the new format, where st_diffs is expected to be nil
-
- expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(8)
- end
-
it 'has the correct data for merge request diff files' do
- expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(9)
+ expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(55)
end
- it 'has the correct data for merge request diff commits in serialised and table formats' do
- expect(MergeRequestDiff.where.not(st_commits: nil).count).to eq(7)
- expect(MergeRequestDiffCommit.count).to eq(6)
- end
-
- it 'has the correct time for merge request st_commits' do
- st_commits = MergeRequestDiff.where.not(st_commits: nil).first.st_commits
-
- expect(st_commits.first[:committed_date]).to be_kind_of(Time)
+ it 'has the correct data for merge request diff commits' do
+ expect(MergeRequestDiffCommit.count).to eq(77)
end
it 'has the correct data for merge request latest_merge_request_diff' do
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index ee173afbd50..6243b6ac9f0 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -93,10 +93,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
end
- it 'has merge requests diff st_diffs' do
- expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil
- end
-
it 'has merge request diff files' do
expect(saved_project_json['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty
end
@@ -172,12 +168,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['custom_attributes'].count).to eq(2)
end
- it 'does not complain about non UTF-8 characters in MR diffs' do
- ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
-
- expect(project_tree_saver.save).to be true
- end
-
it 'does not complain about non UTF-8 characters in MR diff files' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 4e36af18aa7..ec8fa99e0da 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -173,7 +173,6 @@ MergeRequest:
MergeRequestDiff:
- id
- state
-- st_commits
- merge_request_id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 9c3e7d7e9ba..a424f0f5cfe 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -70,6 +70,15 @@ describe Gitlab::ProjectSearchResults do
subject { described_class.parse_search_result(search_result) }
+ it 'can correctly parse filenames including ":"' do
+ special_char_result = "\nmaster:testdata/project::function1.yaml-1----\nmaster:testdata/project::function1.yaml:2:test: data1\n"
+
+ blob = described_class.parse_search_result(special_char_result)
+
+ expect(blob.ref).to eq('master')
+ expect(blob.filename).to eq('testdata/project::function1.yaml')
+ end
+
it "returns a valid FoundBlob" do
is_expected.to be_an Gitlab::SearchResults::FoundBlob
expect(subject.id).to be_nil
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3a19a0753e2..4cf0088ac9c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -557,10 +557,23 @@ describe Ci::Pipeline, :mailer do
describe '#has_kubernetes_active?' do
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns true' do
+ expect(pipeline).to have_kubernetes_active
+ end
+ end
- it 'returns true' do
- expect(pipeline).to have_kubernetes_active
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b91a5e7a272..7f43e747000 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -9,7 +9,6 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
it { is_expected.to delegate_method(:on_creation?).to(:provider) }
- it { is_expected.to delegate_method(:update_kubernetes_integration!).to(:platform) }
it { is_expected.to respond_to :project }
describe '.enabled' do
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index ed76be703a5..53a4e545ff6 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -5,6 +5,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
include ReactiveCachingHelpers
it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
+ it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to respond_to :ca_pem }
describe 'before_validation' do
@@ -90,99 +92,175 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
end
- describe 'after_save from Clusters::Cluster' do
- context 'when platform_kubernetes is being cerated' do
- let(:enabled) { true }
- let(:project) { create(:project) }
- let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, enabled: enabled, projects: [project]) }
- let(:platform) { build(:cluster_platform_kubernetes, :configured) }
- let(:provider) { build(:cluster_provider_gcp) }
- let(:kubernetes_service) { project.kubernetes_service }
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
- it 'updates KubernetesService' do
- cluster.save!
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
- expect(kubernetes_service.active).to eq(enabled)
- expect(kubernetes_service.api_url).to eq(platform.api_url)
- expect(kubernetes_service.namespace).to eq(platform.namespace)
- expect(kubernetes_service.ca_pem).to eq(platform.ca_cert)
- end
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
end
- context 'when platform_kubernetes has been created' do
- let(:enabled) { false }
- let!(:project) { create(:project) }
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:platform) { cluster.platform }
- let(:kubernetes_service) { project.kubernetes_service }
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
- it 'updates KubernetesService' do
- cluster.update(enabled: enabled)
+ describe '#default_namespace' do
+ subject { kubernetes.send(:default_namespace) }
- expect(kubernetes_service.active).to eq(enabled)
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#predefined_variables' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+ let(:api_url) { 'https://kube.domain.com' }
+ let(:ca_pem) { 'CA PEM DATA' }
+ let(:token) { 'token' }
+
+ let(:kubeconfig) do
+ config_file = expand_fixture_path('config/kubeconfig.yml')
+ config = YAML.load(File.read(config_file))
+ config.dig('users', 0, 'user')['token'] = token
+ config.dig('contexts', 0, 'context')['namespace'] = namespace
+ config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
+ Base64.strict_encode64(ca_pem)
+
+ YAML.dump(config)
+ end
+
+ shared_examples 'setting variables' do
+ it 'sets the variables' do
+ expect(kubernetes.predefined_variables).to include(
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
+ { key: 'KUBE_CA_PEM', value: ca_pem, public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ )
end
end
- context 'when kubernetes_service has been configured without cluster integration' do
- let!(:project) { create(:project) }
- let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, projects: [project]) }
- let(:platform) { build(:cluster_platform_kubernetes, :configured, api_url: 'https://111.111.111.111') }
- let(:provider) { build(:cluster_provider_gcp) }
+ context 'namespace is provided' do
+ let(:namespace) { 'my-project' }
before do
- create(:kubernetes_service, project: project)
+ kubernetes.namespace = namespace
end
- it 'raises an error' do
- expect { cluster.save! }.to raise_error('Kubernetes service already configured')
+ it_behaves_like 'setting variables'
+ end
+
+ context 'no namespace provided' do
+ let(:namespace) { kubernetes.actual_namespace }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
end
end
end
- describe '#actual_namespace' do
- subject { kubernetes.actual_namespace }
+ describe '#terminals' do
+ subject { service.terminals(environment) }
- let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:project) { cluster.project }
- let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
- context 'when namespace is present' do
- let(:namespace) { 'namespace-123' }
+ context 'with invalid pods' do
+ it 'returns no terminals' do
+ stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
- it { is_expected.to eq(namespace) }
+ is_expected.to be_empty
+ end
end
- context 'when namespace is not present' do
- let(:namespace) { nil }
+ context 'with valid pods' do
+ let(:pod) { kube_pod(app: environment.slug) }
+ let(:terminals) { kube_terminals(service, pod) }
- it { is_expected.to eq("#{project.path}-#{project.id}") }
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
+ )
+ end
+
+ it 'returns terminals' do
+ is_expected.to eq(terminals + terminals)
+ end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
end
end
- describe '.namespace_for_project' do
- subject { described_class.namespace_for_project(project) }
+ describe '#calculate_reactive_cache' do
+ subject { service.calculate_reactive_cache }
- let(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:enabled) { true }
- it { is_expected.to eq("#{project.path}-#{project.id}") }
- end
+ context 'when cluster is disabled' do
+ let(:enabled) { false }
- describe '#default_namespace' do
- subject { kubernetes.default_namespace }
+ it { is_expected.to be_nil }
+ end
- let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+ context 'when kubernetes responds with valid pods' do
+ before do
+ stub_kubeclient_pods
+ end
- context 'when cluster belongs to a project' do
- let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
- let(:project) { cluster.project }
+ it { is_expected.to eq(pods: [kube_pod]) }
+ end
- it { is_expected.to eq("#{project.path}-#{project.id}") }
+ context 'when kubernetes responds with 500s' do
+ before do
+ stub_kubeclient_pods(status: 500)
+ end
+
+ it { expect { subject }.to raise_error(KubeException) }
end
- context 'when cluster belongs to nothing' do
- let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+ context 'when kubernetes responds with 404s' do
+ before do
+ stub_kubeclient_pods(status: 404)
+ end
- it { is_expected.to be_nil }
+ it { is_expected.to eq(pods: []) }
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 1ce1d595c60..6f24a039998 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -327,15 +327,28 @@ describe Environment do
context 'when the enviroment is available' do
context 'with a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
- context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
- it { is_expected.to be_truthy }
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
end
- context 'but no deployments' do
- it { is_expected.to be_falsy }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -356,7 +369,6 @@ describe Environment do
end
describe '#terminals' do
- let(:project) { create(:kubernetes_project) }
subject { environment.terminals }
context 'when the environment has terminals' do
@@ -364,12 +376,27 @@ describe Environment do
allow(environment).to receive(:has_terminals?).and_return(true)
end
- it 'returns the terminals from the deployment service' do
- expect(project.deployment_service)
- .to receive(:terminals).with(environment)
- .and_return(:fake_terminals)
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns the terminals from the deployment service' do
+ expect(project.deployment_platform)
+ .to receive(:terminals).with(environment)
+ .and_return(:fake_terminals)
+
+ is_expected.to eq(:fake_terminals)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- is_expected.to eq(:fake_terminals)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e2a9233a496..d556004eccf 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe MergeRequestDiff do
+ let(:diff_with_commits) { create(:merge_request).merge_request_diff }
+
describe 'create new record' do
- subject { create(:merge_request).merge_request_diff }
+ subject { diff_with_commits }
it { expect(subject).to be_valid }
it { expect(subject).to be_persisted }
@@ -23,57 +25,41 @@ describe MergeRequestDiff do
end
describe '#diffs' do
- let(:mr) { create(:merge_request, :with_diffs) }
- let(:mr_diff) { mr.merge_request_diff }
-
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
- expect(mr_diff).not_to receive(:load_diffs)
- expect(mr_diff.compare).to receive(:diffs).and_call_original
+ expect(diff_with_commits).not_to receive(:load_diffs)
+ expect(diff_with_commits.compare).to receive(:diffs).and_call_original
- mr_diff.raw_diffs(ignore_whitespace_change: true)
+ diff_with_commits.raw_diffs(ignore_whitespace_change: true)
end
end
context 'when the raw diffs are empty' do
before do
- MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
- end
-
- it 'returns an empty DiffCollection' do
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).to be_empty
- end
- end
-
- context 'when the raw diffs have invalid content' do
- before do
- MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
- mr_diff.update_attributes(st_diffs: ["--broken-diff"])
+ MergeRequestDiffFile.delete_all(merge_request_diff_id: diff_with_commits.id)
end
it 'returns an empty DiffCollection' do
- expect(mr_diff.raw_diffs.to_a).to be_empty
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).to be_empty
+ expect(diff_with_commits.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(diff_with_commits.raw_diffs).to be_empty
end
end
context 'when the raw diffs exist' do
it 'returns the diffs' do
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).not_to be_empty
+ expect(diff_with_commits.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(diff_with_commits.raw_diffs).not_to be_empty
end
context 'when the :paths option is set' do
- let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
+ let(:diffs) { diff_with_commits.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
it 'only returns diffs that match the (old path, new path) given' do
expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
end
it 'uses the diffs from the DB' do
- expect(mr_diff).to receive(:load_diffs)
+ expect(diff_with_commits).to receive(:load_diffs)
diffs
end
@@ -117,51 +103,29 @@ describe MergeRequestDiff do
end
describe '#commit_shas' do
- it 'returns all commits SHA using serialized commits' do
- subject.st_commits = [
- { id: 'sha1' },
- { id: 'sha2' }
- ]
-
- expect(subject.commit_shas).to eq(%w(sha1 sha2))
+ it 'returns all commit SHAs using commits from the DB' do
+ expect(diff_with_commits.commit_shas).not_to be_empty
+ expect(diff_with_commits.commit_shas).to all(match(/\h{40}/))
end
end
describe '#compare_with' do
- subject { create(:merge_request, source_branch: 'fix').merge_request_diff }
-
it 'delegates compare to the service' do
expect(CompareService).to receive(:new).and_call_original
- subject.compare_with(nil)
+ diff_with_commits.compare_with(nil)
end
it 'uses git diff A..B approach by default' do
- diffs = subject.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
+ diffs = diff_with_commits.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
- expect(diffs.size).to eq(3)
+ expect(diffs.size).to eq(21)
end
end
describe '#commits_count' do
it 'returns number of commits using serialized commits' do
- subject.st_commits = [
- { id: 'sha1' },
- { id: 'sha2' }
- ]
-
- expect(subject.commits_count).to eq 2
- end
- end
-
- describe '#utf8_st_diffs' do
- it 'does not raise error when a hash value is in binary' do
- subject.st_diffs = [
- { diff: "\0" },
- { diff: "\x05\x00\x68\x65\x6c\x6c\x6f" }
- ]
-
- expect { subject.utf8_st_diffs }.not_to raise_error
+ expect(diff_with_commits.commits_count).to eq(29)
end
end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 1c629155e1e..f037ee77a94 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -4,8 +4,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
include KubernetesHelpers
include ReactiveCachingHelpers
- let(:project) { build_stubbed(:kubernetes_project) }
- let(:service) { project.kubernetes_service }
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.deployment_platform }
describe 'Associations' do
it { is_expected.to belong_to :project }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 549c97a9afd..a4abcc49a0d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2002,12 +2002,25 @@ describe Project do
end
context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false }
+ )
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
@@ -3083,4 +3096,23 @@ describe Project do
expect(project.wiki_repository_exists?).to eq(false)
end
end
+
+ describe '#deployment_platform' do
+ subject { project.deployment_platform }
+
+ let(:project) { create(:project) }
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let!(:kubernetes_service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to eq(kubernetes_service) }
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+
+ it { is_expected.to eq(platform_kubernetes) }
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 34ecdd1e164..67e1539cbc3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -269,9 +269,8 @@ describe API::Internal do
end
context "git pull" do
- context "gitaly disabled" do
+ context "gitaly disabled", :disable_gitaly do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(false)
pull(key, project)
expect(response).to have_gitlab_http_status(200)
@@ -285,7 +284,6 @@ describe API::Internal do
context "gitaly enabled" do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(true)
pull(key, project)
expect(response).to have_gitlab_http_status(200)
@@ -304,9 +302,8 @@ describe API::Internal do
end
context "git push" do
- context "gitaly disabled" do
+ context "gitaly disabled", :disable_gitaly do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(false)
push(key, project)
expect(response).to have_gitlab_http_status(200)
@@ -320,7 +317,6 @@ describe API::Internal do
context "gitaly enabled" do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(true)
push(key, project)
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 784070db173..3bfb4c5506f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -34,6 +34,48 @@ describe API::Notes do
describe "GET /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: issue, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
@@ -85,6 +127,47 @@ describe API::Notes do
end
context "when noteable is a Snippet" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: snippet, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
it "returns an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
@@ -108,6 +191,47 @@ describe API::Notes do
end
context "when noteable is a Merge Request" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: merge_request, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
it "returns an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user)
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
index 5ec8ed0976d..090b2dcdd43 100644
--- a/spec/services/base_count_service_spec.rb
+++ b/spec/services/base_count_service_spec.rb
@@ -77,4 +77,10 @@ describe BaseCountService, :use_clean_rails_memory_store_caching do
expect { service.cache_key }.to raise_error(NotImplementedError)
end
end
+
+ describe '#cache_options' do
+ it 'returns the default in options' do
+ expect(service.cache_options).to eq({ raw: false })
+ end
+ end
end
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 313f87ae1f6..a7ab389b357 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -6,8 +6,10 @@ describe MergeRequests::CreateFromIssueService do
let(:label_ids) { create_pair(:label, project: project).map(&:id) }
let(:milestone_id) { create(:milestone, project: project).id }
let(:issue) { create(:issue, project: project, milestone_id: milestone_id) }
+ let(:custom_source_branch) { 'custom-source-branch' }
subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
+ subject(:service_with_custom_source_branch) { described_class.new(project, user, issue_iid: issue.iid, branch_name: custom_source_branch) }
before do
project.add_developer(user)
@@ -17,8 +19,8 @@ describe MergeRequests::CreateFromIssueService do
it 'returns an error with invalid issue iid' do
result = described_class.new(project, user, issue_iid: -1).execute
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'Invalid issue iid'
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Invalid issue iid')
end
it 'delegates issue search to IssuesFinder' do
@@ -53,6 +55,12 @@ describe MergeRequests::CreateFromIssueService do
expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
end
+ it 'creates a branch using passed name' do
+ service_with_custom_source_branch.execute
+
+ expect(project.repository.branch_exists?(custom_source_branch)).to be_truthy
+ end
+
it 'creates a system note' do
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
@@ -72,19 +80,25 @@ describe MergeRequests::CreateFromIssueService do
it 'sets the merge request author to current user' do
result = service.execute
- expect(result[:merge_request].author).to eq user
+ expect(result[:merge_request].author).to eq(user)
end
it 'sets the merge request source branch to the new issue branch' do
result = service.execute
- expect(result[:merge_request].source_branch).to eq issue.to_branch_name
+ expect(result[:merge_request].source_branch).to eq(issue.to_branch_name)
+ end
+
+ it 'sets the merge request source branch to the passed branch name' do
+ result = service_with_custom_source_branch.execute
+
+ expect(result[:merge_request].source_branch).to eq(custom_source_branch)
end
it 'sets the merge request target branch to the project default branch' do
result = service.execute
- expect(result[:merge_request].target_branch).to eq project.default_branch
+ expect(result[:merge_request].target_branch).to eq(project.default_branch)
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6310ea1b52b..242a2230b67 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -207,3 +207,6 @@ Shoulda::Matchers.configure do |config|
with.library :rails
end
end
+
+# Prevent Rugged from picking up local developer gitconfig.
+Rugged::Settings['search_path_global'] = Rails.root.join('tmp/tests').to_s
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 9f672bc92fc..935b170a0f6 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,21 +7,41 @@ require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :chrome
Capybara.register_driver :chrome do |app|
- extra_args = []
- extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
-
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- chromeOptions: {
- 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
}
)
- Capybara::Selenium::Driver
- .new(app, browser: :chrome, desired_capabilities: capabilities)
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument("window-size=1240,1400")
+
+ # Chrome won't work properly in a Docker container in sandbox mode
+ options.add_argument("no-sandbox")
+
+ # Run headless by default unless CHROME_HEADLESS specified
+ unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+ options.add_argument("headless")
+
+ # Chrome documentation says this flag is needed for now
+ # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
+ options.add_argument("disable-gpu")
+ end
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :chrome,
+ desired_capabilities: capabilities,
+ options: options
+ )
end
+Capybara.javascript_driver = :chrome
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb
index 620fa37d455..dbbd4ad4d40 100644
--- a/spec/support/prometheus/additional_metrics_shared_examples.rb
+++ b/spec/support/prometheus/additional_metrics_shared_examples.rb
@@ -41,16 +41,30 @@ RSpec.shared_examples 'additional metrics query' do
end
describe 'project has Kubernetes service' do
- let(:project) { create(:kubernetes_project) }
- let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
- let(:kube_namespace) { project.kubernetes_service.actual_namespace }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
+ let(:kube_namespace) { project.deployment_platform.actual_namespace }
- it_behaves_like 'query context containing environment slug and filter'
+ it_behaves_like 'query context containing environment slug and filter'
- it 'query context contains kube_namespace' do
- expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
+ it 'query context contains kube_namespace' do
+ expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
- subject.query(*query_params)
+ subject.query(*query_params)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
index c757ccf02d3..95f0be49412 100644
--- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -35,7 +35,7 @@ describe 'projects/pipelines_settings/_show' do
context 'when kubernetes is active' do
before do
- project.build_kubernetes_service(active: true)
+ create(:kubernetes_service, project: project)
end
context 'when auto devops domain is not defined' do
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 5f4453c15d6..3da851de067 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -1,15 +1,28 @@
require 'spec_helper'
describe ReactiveCachingWorker do
- let(:project) { create(:kubernetes_project) }
- let(:service) { project.deployment_service }
- subject { described_class.new.perform("KubernetesService", service.id) }
+ let(:service) { project.deployment_platform }
describe '#perform' do
- it 'calls #exclusively_update_reactive_cache!' do
- expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
- subject
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("KubernetesService", service.id)
+ end
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(Clusters::Platforms::Kubernetes).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("Clusters::Platforms::Kubernetes", service.id)
+ end
end
end
end
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
deleted file mode 100644
index 39d7d2306f8..00000000000
--- a/vendor/assets/javascripts/clipboard.js
+++ /dev/null
@@ -1,621 +0,0 @@
-/*!
- * clipboard.js v1.4.2
- * https://zenorocha.github.io/clipboard.js
- *
- * Licensed MIT © Zeno Rocha
- */
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/**
- * Module dependencies.
- */
-
-var closest = require('closest')
- , event = require('component-event');
-
-/**
- * Delegate event `type` to `selector`
- * and invoke `fn(e)`. A callback function
- * is returned which may be passed to `.unbind()`.
- *
- * @param {Element} el
- * @param {String} selector
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-// Some events don't bubble, so we want to bind to the capture phase instead
-// when delegating.
-var forceCaptureEvents = ['focus', 'blur'];
-
-exports.bind = function(el, selector, type, fn, capture){
- if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
-
- return event.bind(el, type, function(e){
- var target = e.target || e.srcElement;
- e.delegateTarget = closest(target, selector, true, el);
- if (e.delegateTarget) fn.call(el, e);
- }, capture);
-};
-
-/**
- * Unbind event `type`'s callback `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @api public
- */
-
-exports.unbind = function(el, type, fn, capture){
- if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
-
- event.unbind(el, type, fn, capture);
-};
-
-},{"closest":2,"component-event":4}],2:[function(require,module,exports){
-var matches = require('matches-selector')
-
-module.exports = function (element, selector, checkYoSelf) {
- var parent = checkYoSelf ? element : element.parentNode
-
- while (parent && parent !== document) {
- if (matches(parent, selector)) return parent;
- parent = parent.parentNode
- }
-}
-
-},{"matches-selector":3}],3:[function(require,module,exports){
-
-/**
- * Element prototype.
- */
-
-var proto = Element.prototype;
-
-/**
- * Vendor function.
- */
-
-var vendor = proto.matchesSelector
- || proto.webkitMatchesSelector
- || proto.mozMatchesSelector
- || proto.msMatchesSelector
- || proto.oMatchesSelector;
-
-/**
- * Expose `match()`.
- */
-
-module.exports = match;
-
-/**
- * Match `el` to `selector`.
- *
- * @param {Element} el
- * @param {String} selector
- * @return {Boolean}
- * @api public
- */
-
-function match(el, selector) {
- if (vendor) return vendor.call(el, selector);
- var nodes = el.parentNode.querySelectorAll(selector);
- for (var i = 0; i < nodes.length; ++i) {
- if (nodes[i] == el) return true;
- }
- return false;
-}
-},{}],4:[function(require,module,exports){
-var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
- unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
- prefix = bind !== 'addEventListener' ? 'on' : '';
-
-/**
- * Bind `el` event `type` to `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-exports.bind = function(el, type, fn, capture){
- el[bind](prefix + type, fn, capture || false);
- return fn;
-};
-
-/**
- * Unbind `el` event `type`'s callback `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-exports.unbind = function(el, type, fn, capture){
- el[unbind](prefix + type, fn, capture || false);
- return fn;
-};
-},{}],5:[function(require,module,exports){
-function E () {
- // Keep this empty so it's easier to inherit from
- // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
-}
-
-E.prototype = {
- on: function (name, callback, ctx) {
- var e = this.e || (this.e = {});
-
- (e[name] || (e[name] = [])).push({
- fn: callback,
- ctx: ctx
- });
-
- return this;
- },
-
- once: function (name, callback, ctx) {
- var self = this;
- var fn = function () {
- self.off(name, fn);
- callback.apply(ctx, arguments);
- };
-
- return this.on(name, fn, ctx);
- },
-
- emit: function (name) {
- var data = [].slice.call(arguments, 1);
- var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
- var i = 0;
- var len = evtArr.length;
-
- for (i; i < len; i++) {
- evtArr[i].fn.apply(evtArr[i].ctx, data);
- }
-
- return this;
- },
-
- off: function (name, callback) {
- var e = this.e || (this.e = {});
- var evts = e[name];
- var liveEvents = [];
-
- if (evts && callback) {
- for (var i = 0, len = evts.length; i < len; i++) {
- if (evts[i].fn !== callback) liveEvents.push(evts[i]);
- }
- }
-
- // Remove event from queue to prevent memory leak
- // Suggested by https://github.com/lazd
- // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
-
- (liveEvents.length)
- ? e[name] = liveEvents
- : delete e[name];
-
- return this;
- }
-};
-
-module.exports = E;
-
-},{}],6:[function(require,module,exports){
-/**
- * Inner class which performs selection from either `text` or `target`
- * properties and then executes copy or cut operations.
- */
-'use strict';
-
-exports.__esModule = true;
-
-var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
-
-var ClipboardAction = (function () {
- /**
- * @param {Object} options
- */
-
- function ClipboardAction(options) {
- _classCallCheck(this, ClipboardAction);
-
- this.resolveOptions(options);
- this.initSelection();
- }
-
- /**
- * Defines base properties passed from constructor.
- * @param {Object} options
- */
-
- ClipboardAction.prototype.resolveOptions = function resolveOptions() {
- var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
-
- this.action = options.action;
- this.emitter = options.emitter;
- this.target = options.target;
- this.text = options.text;
- this.trigger = options.trigger;
-
- this.selectedText = '';
- };
-
- /**
- * Decides which selection strategy is going to be applied based
- * on the existence of `text` and `target` properties.
- */
-
- ClipboardAction.prototype.initSelection = function initSelection() {
- if (this.text && this.target) {
- throw new Error('Multiple attributes declared, use either "target" or "text"');
- } else if (this.text) {
- this.selectFake();
- } else if (this.target) {
- this.selectTarget();
- } else {
- throw new Error('Missing required attributes, use either "target" or "text"');
- }
- };
-
- /**
- * Creates a fake textarea element, sets its value from `text` property,
- * and makes a selection on it.
- */
-
- ClipboardAction.prototype.selectFake = function selectFake() {
- var _this = this;
-
- this.removeFake();
-
- this.fakeHandler = document.body.addEventListener('click', function () {
- return _this.removeFake();
- });
-
- this.fakeElem = document.createElement('textarea');
- this.fakeElem.style.position = 'absolute';
- this.fakeElem.style.left = '-9999px';
- this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
- this.fakeElem.setAttribute('readonly', '');
- this.fakeElem.value = this.text;
- this.selectedText = this.text;
-
- document.body.appendChild(this.fakeElem);
-
- this.fakeElem.select();
- this.copyText();
- };
-
- /**
- * Only removes the fake element after another click event, that way
- * a user can hit `Ctrl+C` to copy because selection still exists.
- */
-
- ClipboardAction.prototype.removeFake = function removeFake() {
- if (this.fakeHandler) {
- document.body.removeEventListener('click');
- this.fakeHandler = null;
- }
-
- if (this.fakeElem) {
- document.body.removeChild(this.fakeElem);
- this.fakeElem = null;
- }
- };
-
- /**
- * Selects the content from element passed on `target` property.
- */
-
- ClipboardAction.prototype.selectTarget = function selectTarget() {
- if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
- this.target.select();
- this.selectedText = this.target.value;
- } else {
- var range = document.createRange();
- var selection = window.getSelection();
-
- selection.removeAllRanges();
- range.selectNodeContents(this.target);
- selection.addRange(range);
- this.selectedText = selection.toString();
- }
-
- this.copyText();
- };
-
- /**
- * Executes the copy operation based on the current selection.
- */
-
- ClipboardAction.prototype.copyText = function copyText() {
- var succeeded = undefined;
-
- try {
- succeeded = document.execCommand(this.action);
- } catch (err) {
- succeeded = false;
- }
-
- this.handleResult(succeeded);
- };
-
- /**
- * Fires an event based on the copy operation result.
- * @param {Boolean} succeeded
- */
-
- ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
- if (succeeded) {
- this.emitter.emit('success', {
- action: this.action,
- text: this.selectedText,
- trigger: this.trigger,
- clearSelection: this.clearSelection.bind(this)
- });
- } else {
- this.emitter.emit('error', {
- action: this.action,
- trigger: this.trigger,
- clearSelection: this.clearSelection.bind(this)
- });
- }
- };
-
- /**
- * Removes current selection and focus from `target` element.
- */
-
- ClipboardAction.prototype.clearSelection = function clearSelection() {
- if (this.target) {
- this.target.blur();
- }
-
- window.getSelection().removeAllRanges();
- };
-
- /**
- * Sets the `action` to be performed which can be either 'copy' or 'cut'.
- * @param {String} action
- */
-
- /**
- * Destroy lifecycle.
- */
-
- ClipboardAction.prototype.destroy = function destroy() {
- this.removeFake();
- };
-
- _createClass(ClipboardAction, [{
- key: 'action',
- set: function set() {
- var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
-
- this._action = action;
-
- if (this._action !== 'copy' && this._action !== 'cut') {
- throw new Error('Invalid "action" value, use either "copy" or "cut"');
- }
- },
-
- /**
- * Gets the `action` property.
- * @return {String}
- */
- get: function get() {
- return this._action;
- }
-
- /**
- * Sets the `target` property using an element
- * that will be have its content copied.
- * @param {Element} target
- */
- }, {
- key: 'target',
- set: function set(target) {
- if (target !== undefined) {
- if (target && typeof target === 'object' && target.nodeType === 1) {
- this._target = target;
- } else {
- throw new Error('Invalid "target" value, use a valid Element');
- }
- }
- },
-
- /**
- * Gets the `target` property.
- * @return {String|HTMLElement}
- */
- get: function get() {
- return this._target;
- }
- }]);
-
- return ClipboardAction;
-})();
-
-exports['default'] = ClipboardAction;
-module.exports = exports['default'];
-
-},{}],7:[function(require,module,exports){
-'use strict';
-
-exports.__esModule = true;
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
-
-function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
-
-var _clipboardAction = require('./clipboard-action');
-
-var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
-
-var _delegateEvents = require('delegate-events');
-
-var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
-
-var _tinyEmitter = require('tiny-emitter');
-
-var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
-
-/**
- * Base class which takes a selector, delegates a click event to it,
- * and instantiates a new `ClipboardAction` on each click.
- */
-
-var Clipboard = (function (_Emitter) {
- _inherits(Clipboard, _Emitter);
-
- /**
- * @param {String} selector
- * @param {Object} options
- */
-
- function Clipboard(selector, options) {
- _classCallCheck(this, Clipboard);
-
- _Emitter.call(this);
-
- this.resolveOptions(options);
- this.delegateClick(selector);
- }
-
- /**
- * Helper function to retrieve attribute value.
- * @param {String} suffix
- * @param {Element} element
- */
-
- /**
- * Defines if attributes would be resolved using internal setter functions
- * or custom functions that were passed in the constructor.
- * @param {Object} options
- */
-
- Clipboard.prototype.resolveOptions = function resolveOptions() {
- var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
-
- this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
- this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
- this.text = typeof options.text === 'function' ? options.text : this.defaultText;
- };
-
- /**
- * Delegates a click event on the passed selector.
- * @param {String} selector
- */
-
- Clipboard.prototype.delegateClick = function delegateClick(selector) {
- var _this = this;
-
- this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
- return _this.onClick(e);
- });
- };
-
- /**
- * Undelegates a click event on body.
- * @param {String} selector
- */
-
- Clipboard.prototype.undelegateClick = function undelegateClick() {
- _delegateEvents2['default'].unbind(document.body, 'click', this.binding);
- };
-
- /**
- * Defines a new `ClipboardAction` on each click event.
- * @param {Event} e
- */
-
- Clipboard.prototype.onClick = function onClick(e) {
- if (this.clipboardAction) {
- this.clipboardAction = null;
- }
-
- this.clipboardAction = new _clipboardAction2['default']({
- action: this.action(e.delegateTarget),
- target: this.target(e.delegateTarget),
- text: this.text(e.delegateTarget),
- trigger: e.delegateTarget,
- emitter: this
- });
- };
-
- /**
- * Default `action` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultAction = function defaultAction(trigger) {
- return getAttributeValue('action', trigger);
- };
-
- /**
- * Default `target` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
- var selector = getAttributeValue('target', trigger);
-
- if (selector) {
- return document.querySelector(selector);
- }
- };
-
- /**
- * Default `text` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultText = function defaultText(trigger) {
- return getAttributeValue('text', trigger);
- };
-
- /**
- * Destroy lifecycle.
- */
-
- Clipboard.prototype.destroy = function destroy() {
- this.undelegateClick();
-
- if (this.clipboardAction) {
- this.clipboardAction.destroy();
- this.clipboardAction = null;
- }
- };
-
- return Clipboard;
-})(_tinyEmitter2['default']);
-
-function getAttributeValue(suffix, element) {
- var attribute = 'data-clipboard-' + suffix;
-
- if (!element.hasAttribute(attribute)) {
- return;
- }
-
- return element.getAttribute(attribute);
-}
-
-exports['default'] = Clipboard;
-module.exports = exports['default'];
-
-},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
-});
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index 69e35e6aa40..7a811e1986b 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index 561b1e5902c..7db63ecc65f 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 0ba6ec7c60c..96f51ee804c 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 73cc4f11500..88897476a15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -116,6 +116,15 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
+ajv@^5.0.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
ajv@^5.1.5:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
@@ -1288,13 +1297,13 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
-clipboard@^1.5.5:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+clipboard@^1.5.5, clipboard@^1.7.1:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
dependencies:
- good-listener "^1.2.0"
+ good-listener "^1.2.2"
select "^1.1.2"
- tiny-emitter "^1.0.0"
+ tiny-emitter "^2.0.0"
cliui@^2.1.0:
version "2.1.0"
@@ -1895,6 +1904,10 @@ di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+diff@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -2527,6 +2540,10 @@ fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -2883,7 +2900,7 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-good-listener@^1.2.0:
+good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
@@ -3843,7 +3860,7 @@ loader-utils@^0.2.15, loader-utils@^0.2.5:
json5 "^0.5.0"
object-assign "^4.0.1"
-loader-utils@^1.0.2, loader-utils@^1.1.0:
+loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
dependencies:
@@ -5534,6 +5551,12 @@ sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
+schema-utils@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
+ dependencies:
+ ajv "^5.0.0"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -6115,9 +6138,9 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
-tiny-emitter@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
+tiny-emitter@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
tmp@0.0.31, tmp@0.0.x:
version "0.0.31"
@@ -6601,6 +6624,13 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+worker-loader@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809"
+ dependencies:
+ loader-utils "^1.0.0"
+ schema-utils "^0.3.0"
+
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"