summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml9
-rw-r--r--CHANGELOG.md22
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--PROCESS.md38
-rw-r--r--app/assets/javascripts/abuse_reports.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js3
-rw-r--r--app/assets/javascripts/commits.js4
-rw-r--r--app/assets/javascripts/create_label.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js4
-rw-r--r--app/assets/javascripts/dispatcher.js10
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/gl_form.js5
-rw-r--r--app/assets/javascripts/issue.js4
-rw-r--r--app/assets/javascripts/job.js50
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js28
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js153
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js228
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/members.js12
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue4
-rw-r--r--app/assets/javascripts/project.js246
-rw-r--r--app/assets/javascripts/project_avatar.js31
-rw-r--r--app/assets/javascripts/project_import.js17
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue7
-rw-r--r--app/assets/javascripts/repo/stores/actions.js4
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/wikis.js3
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blank.scss74
-rw-r--r--app/assets/stylesheets/framework/header.scss22
-rw-r--r--app/assets/stylesheets/framework/mixins.scss28
-rw-r--r--app/assets/stylesheets/framework/popup.scss15
-rw-r--r--app/assets/stylesheets/framework/variables.scss7
-rw-r--r--app/assets/stylesheets/pages/settings.scss19
-rw-r--r--app/controllers/concerns/lfs_request.rb10
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/projects/jobs_controller.rb7
-rw-r--r--app/finders/autocomplete_users_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/helpers/icons_helper.rb9
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/concerns/avatarable.rb25
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/diff_note.rb3
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/lfs_object.rb10
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/project.rb12
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/policies/ci/build_policy.rb11
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb1
-rw-r--r--app/services/ci/pipeline_trigger_service.rb8
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/issues/base_service.rb4
-rw-r--r--app/services/merge_requests/merge_service.rb27
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml70
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml98
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml21
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml22
-rw-r--r--app/views/notify/_note_email.html.haml11
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml2
-rw-r--r--app/views/shared/icons/_add_new_project.svg2
-rw-r--r--app/views/shared/icons/_icon_hourglass.svg1
-rw-r--r--app/views/shared/icons/_lightbulb.svg1
-rw-r--r--app/views/shared/members/_member.html.haml1
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml16
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml1
-rw-r--r--app/workers/update_merge_requests_worker.rb9
-rw-r--r--changelogs/unreleased/.yml5
-rw-r--r--changelogs/unreleased/3615-improve-welcome-screen.yml5
-rw-r--r--changelogs/unreleased/38075_allow_refernce_integer_labels.yml5
-rw-r--r--changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml5
-rw-r--r--changelogs/unreleased/39109-reenable-scroll-job.yml5
-rw-r--r--changelogs/unreleased/39335-add-time-spend-to-milestones.yml5
-rw-r--r--changelogs/unreleased/39436-pages-api-administrative.yml5
-rw-r--r--changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/39704_fix_webhooks_log_time.yml5
-rw-r--r--changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml6
-rw-r--r--changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml5
-rw-r--r--changelogs/unreleased/40068-runner-sorting-regression.yml5
-rw-r--r--changelogs/unreleased/40092-fix-cluster-size.yml5
-rw-r--r--changelogs/unreleased/add-typescript.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml5
-rw-r--r--changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml5
-rw-r--r--changelogs/unreleased/bvl-unlink-fixes.yml5
-rw-r--r--changelogs/unreleased/cleanup-issues-schema.yml5
-rw-r--r--changelogs/unreleased/dm-authorize-admin-oauth-application.yml6
-rw-r--r--changelogs/unreleased/dm-avatarable-with-asset-host.yml6
-rw-r--r--changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml6
-rw-r--r--changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml6
-rw-r--r--changelogs/unreleased/fix-filter-by-my-reaction.yml5
-rw-r--r--changelogs/unreleased/fix-import-export-arguments.yml5
-rw-r--r--changelogs/unreleased/fix-mysql-grant-check.yml5
-rw-r--r--changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml5
-rw-r--r--changelogs/unreleased/fix-subgroup-autocomplete.yml5
-rw-r--r--changelogs/unreleased/fix-todos-last-page.yml5
-rw-r--r--changelogs/unreleased/fix_diff_parsing.yml5
-rw-r--r--changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml5
-rw-r--r--changelogs/unreleased/issue_39176.yml5
-rw-r--r--changelogs/unreleased/issue_39238.yml5
-rw-r--r--changelogs/unreleased/issue_40058.yml5
-rw-r--r--changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml5
-rw-r--r--changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml5
-rw-r--r--changelogs/unreleased/sh-fix-environment-slug-generation.yml5
-rw-r--r--changelogs/unreleased/text-utils.yml5
-rw-r--r--changelogs/unreleased/winh-subgroups-api.yml5
-rw-r--r--config/locales/doorkeeper.en.yml10
-rw-r--r--config/webpack.config.js6
-rw-r--r--db/migrate/20171106132212_issues_confidential_not_null.rb23
-rw-r--r--db/migrate/20171106135924_issues_milestone_id_foreign_key.rb38
-rw-r--r--db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb45
-rw-r--r--db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb44
-rw-r--r--db/migrate/20171106154015_remove_issues_branch_name.rb13
-rw-r--r--db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb37
-rw-r--r--db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/schema.rb14
-rw-r--r--doc/api/groups.md49
-rw-r--r--doc/api/pages_domains.md25
-rw-r--r--doc/api/services.md37
-rw-r--r--doc/development/limit_ee_conflicts.md6
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/university/glossary/README.md149
-rw-r--r--doc/update/10.1-to-10.2.md360
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--lib/api/api_guard.rb8
-rw-r--r--lib/api/entities.rb20
-rw-r--r--lib/api/groups.rb66
-rw-r--r--lib/api/helpers.rb9
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/pages_domains.rb22
-rw-r--r--lib/api/services.rb6
-rw-r--r--lib/api/v3/builds.rb2
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb12
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb5
-rw-r--r--lib/gitlab/git/operation_service.rb10
-rw-r--r--lib/gitlab/git/remote_repository.rb82
-rw-r--r--lib/gitlab/git/repository.rb36
-rw-r--r--lib/gitlab/git/wiki.rb18
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb4
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb58
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb1
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/utils/strong_memoize.rb31
-rw-r--r--package.json2
-rw-r--r--qa/.gitignore1
-rwxr-xr-xqa/bin/qa2
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/base.rb2
-rw-r--r--qa/qa/page/main/entry.rb21
-rw-r--r--qa/qa/page/main/login.rb19
-rw-r--r--qa/qa/page/mattermost/login.rb2
-rw-r--r--qa/qa/page/mattermost/main.rb2
-rw-r--r--qa/qa/runtime/scenario.rb22
-rw-r--r--qa/qa/scenario/bootable.rb45
-rw-r--r--qa/qa/scenario/entrypoint.rb28
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb3
-rw-r--r--qa/qa/specs/config.rb9
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb3
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb3
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb3
-rw-r--r--qa/qa/specs/features/project/create_spec.rb3
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb3
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb3
-rw-r--r--qa/qa/specs/runner.rb22
-rw-r--r--qa/spec/runtime/scenario_spec.rb27
-rw-r--r--qa/spec/scenario/bootable_spec.rb24
-rw-r--r--qa/spec/scenario/entrypoint_spec.rb20
-rw-r--r--scripts/create_mysql_user.sh8
-rw-r--r--scripts/create_postgres_user.sh8
-rw-r--r--scripts/prepare_build.sh14
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb55
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb4
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb25
-rw-r--r--spec/features/groups/members/manage_members.rb6
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb18
-rw-r--r--spec/features/issues/issue_detail_spec.rb7
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb22
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb16
-rw-r--r--spec/features/milestone_spec.rb29
-rw-r--r--spec/features/projects/members/list_spec.rb16
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb15
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json18
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domains.json21
-rw-r--r--spec/helpers/application_helper_spec.rb73
-rw-r--r--spec/helpers/groups_helper_spec.rb32
-rw-r--r--spec/helpers/icons_helper_spec.rb28
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js19
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js22
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js27
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js116
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js19
-rw-r--r--spec/javascripts/repo/helpers.js5
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js38
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js417
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js469
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js419
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js119
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js131
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js71
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js117
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js102
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb9
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb43
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb74
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb99
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb70
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb88
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb40
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb52
-rw-r--r--spec/mailers/notify_spec.rb20
-rw-r--r--spec/models/ci/build_spec.rb17
-rw-r--r--spec/models/concerns/avatarable_spec.rb44
-rw-r--r--spec/models/concerns/milestoneish_spec.rb17
-rw-r--r--spec/models/diff_note_spec.rb6
-rw-r--r--spec/models/group_spec.rb10
-rw-r--r--spec/models/issue_spec.rb18
-rw-r--r--spec/models/merge_request_spec.rb12
-rw-r--r--spec/models/project_spec.rb30
-rw-r--r--spec/models/user_spec.rb11
-rw-r--r--spec/policies/ci/build_policy_spec.rb77
-rw-r--r--spec/requests/api/groups_spec.rb136
-rw-r--r--spec/requests/api/jobs_spec.rb21
-rw-r--r--spec/requests/api/pages_domains_spec.rb47
-rw-r--r--spec/requests/api/services_spec.rb21
-rw-r--r--spec/requests/api/v3/builds_spec.rb2
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb24
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb22
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--tsconfig.json8
-rw-r--r--vendor/assets/javascripts/latinise.js11
-rw-r--r--vendor/gitignore/Android.gitignore2
-rw-r--r--vendor/gitignore/Perl.gitignore2
-rw-r--r--vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml2
-rw-r--r--vendor/licenses.csv228
-rw-r--r--yarn.lock195
261 files changed, 6017 insertions, 1374 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6a5050b553f..65f2bc7045f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -416,12 +416,8 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
- allow_failure: yes
+ allow_failure: no
retry: 0
- cache:
- key: "ee_compat_check_repo"
- paths:
- - ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
@@ -453,6 +449,7 @@ db:migrate:reset-mysql:
stage: test
variables:
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
@@ -497,6 +494,7 @@ db:rollback-mysql:
variables:
SIZE: "1"
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
@@ -532,7 +530,6 @@ gitlab:assets:compile:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "false"
- USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f13eca2caf..4294ccaf9b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.1.3 (2017-11-10)
+
+- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
+- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
+- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
+- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
+- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
+- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
+- [FIXED] Fix arguments Import/Export error importing project merge requests.
+- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
+- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
+- [FIXED] Render 404 when polling commit notes without having permissions.
+- [FIXED] Show error message when fast-forward merge is not possible.
+- [FIXED] Avoid regenerating the ref path for the environment.
+- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
+
+## 10.1.2 (2017-11-08)
+
+- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
+- [SECURITY] Properly translate IP addresses written in decimal, octal, or other formats in SSRF protections in project imports.
+- [FIXED] Fix TRIGGER checks for MySQL.
+
## 10.1.1 (2017-10-31)
- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 4f9b378b40f..7f422a161ae 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.52.0
+0.53.0
diff --git a/Gemfile b/Gemfile
index 63d3d214a5a..c495e4eceba 100644
--- a/Gemfile
+++ b/Gemfile
@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index ae145ca5f69..43555a01037 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -274,7 +274,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.51.0)
+ gitaly-proto (0.52.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -1026,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.51.0)
+ gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1185,4 +1185,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.4
+ 1.16.0
diff --git a/PROCESS.md b/PROCESS.md
index 06963243b25..7c8db689256 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -141,21 +141,29 @@ the stable branch are:
* Fixes for security issues
* New or updated translations (as long as they do not touch application code)
-During the feature freeze all merge requests that are meant to go into the upcoming
-release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set, so that release managers can find and pick them.
-Merge requests without a milestone and this label will
-not be merged into any stable branches.
-
-Fixes marked like this will be shipped in the next RC for that release. Once
-the final RC has been prepared ready for release on the 22nd, further fixes
-marked ~"Pick into Stable" will go into a patch for that release.
-
-If a merge request is to be picked into more than one release it will also need
-the ~"Pick into Backports" label set to remind the release manager to change
-the milestone after cherry-picking. As before, it should still have the
-~"Pick into Stable" label and the milestone of the highest release it will be
-picked into.
+During the feature freeze all merge requests that are meant to go into the
+upcoming release should have the correct milestone assigned _and_ the
+`Pick into X.Y` label where `X.Y` is equal to the milestone, so that release
+managers can find and pick them.
+Merge requests without this label will not be picked into the stable release.
+
+For example, if the upcoming release is `10.2.0` you will need to set the
+`Pick into 10.2` label.
+
+Fixes marked like this will be shipped in the next RC (before the 22nd), or the
+next patch release.
+
+If a merge request is to be picked into more than one release it will need one
+`Pick into X.Y` label per release where the merge request should be back-ported
+to.
+
+For example, if the current patch release is `10.1.1` and a regression fix needs
+to be backported down to the `9.5` release, you will need to assign it the
+`10.1` milestone and the following labels:
+
+- `Pick into 10.1`
+- `Pick into 10.0`
+- `Pick into 9.5`
### Asking for an exception
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 3de192d56eb..d2d3a257c0d 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,3 +1,5 @@
+import { truncate } from './lib/utils/text_utility';
+
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index de9e44cef35..182957113a2 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -3,6 +3,7 @@
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
+import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() {
const count = ModalStore.selectedCount();
- return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
},
},
methods: {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ae6b8902032..9b952ea7b60 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
+import { pluralize } from './lib/utils/text_utility';
+
export default (function () {
const CommitsList = {};
@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
- $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
+ $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 3bed0678350..9a4c9bfcc80 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
+import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 8bf9ae17de0..a8cd8c20f8f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
-import '../lib/utils/text_utility';
+import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
@@ -36,7 +36,7 @@ export default {
});
newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.name.toLowerCase());
+ const stageSlug = dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 44606989395..d716218d9a4 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -20,15 +20,15 @@ import groupsSelect from './groups_select';
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
-/* global Project */
-/* global ProjectAvatar */
+import Project from './project';
+import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
/* global ProjectNew */
/* global ProjectShow */
-/* global ProjectImport */
+import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
@@ -378,7 +378,7 @@ import Diff from './diff';
initSettingsPanels();
break;
case 'projects:imports:show':
- new ProjectImport();
+ projectImport();
break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
@@ -604,7 +604,7 @@ import Diff from './diff';
break;
case 'projects':
new Project();
- new ProjectAvatar();
+ projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index fc0308b81ba..9d25f806c0d 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -2,7 +2,7 @@
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import '../../lib/utils/text_utility';
+import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
- name: gl.text.humanize(action.name),
+ name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 5c624b79d45..a642464c920 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) {
+ const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index c4202f92443..4e7a6e54f90 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
- return $(selector);
+ return $(selector, this.instance.dropdown);
};
})(this),
data: (function(_this) {
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 48cd43d3348..d0f9e6af0f8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
+import textUtils from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
@@ -46,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
- gl.text.init(this.form);
+ textUtils.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
- gl.text.removeListeners(this.form);
+ textUtils.removeListeners(this.form);
}
addEventListeners() {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index acd5730cf3c..7de07e9403d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
-import '~/lib/utils/text_utility';
+import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
- projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+ projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index c6b5844dff6..cf8fda9a4fa 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -14,8 +14,8 @@ export default class Job {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
+ this.$window = $(window);
this.logBytes = 0;
- this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
@@ -54,23 +54,18 @@ export default class Job {
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
- $(window)
+ this.$window
.off('scroll')
.on('scroll', () => {
- const contentHeight = this.$buildTraceOutput.height();
- if (contentHeight > this.windowSize) {
- // means the user did not scroll, the content was updated.
- this.windowSize = contentHeight;
- } else {
- // User scrolled
- this.hasBeenScrolled = true;
+ if (!this.isScrolledToBottom()) {
this.toggleScrollAnimation(false);
+ } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ this.toggleScrollAnimation(true);
}
-
this.scrollThrottled();
});
- $(window)
+ this.$window
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
@@ -99,14 +94,14 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
- return $(document).height() > $(window).height();
+ return this.$document.height() > this.$window.height();
}
toggleScroll() {
- const currentPosition = $(document).scrollTop();
- const scrollHeight = $(document).height();
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
- const windowHeight = $(window).height();
+ const windowHeight = this.$window.height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
@@ -119,7 +114,7 @@ export default class Job {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (scrollHeight - currentPosition === windowHeight) {
+ } else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
@@ -131,9 +126,17 @@ export default class Job {
}
}
+ isScrolledToBottom() {
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
+
+ const windowHeight = this.$window.height();
+ return scrollHeight - currentPosition === windowHeight;
+ }
+
// eslint-disable-next-line class-methods-use-this
scrollDown() {
- $(document).scrollTop($(document).height());
+ this.$document.scrollTop(this.$document.height());
}
scrollToBottom() {
@@ -143,7 +146,7 @@ export default class Job {
}
scrollToTop() {
- $(document).scrollTop(0);
+ this.$document.scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -174,7 +177,7 @@ export default class Job {
this.state = log.state;
}
- this.windowSize = this.$buildTraceOutput.height();
+ this.isScrollInBottom = this.isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
@@ -194,14 +197,9 @@ export default class Job {
} else {
this.$truncatedInfo.addClass('hidden');
}
+ this.isLogComplete = log.complete;
if (!log.complete) {
- if (!this.hasBeenScrolled) {
- this.toggleScrollAnimation(true);
- } else {
- this.toggleScrollAnimation(false);
- }
-
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
@@ -218,7 +216,7 @@ export default class Job {
this.$buildRefreshAnimation.remove();
})
.then(() => {
- if (!this.hasBeenScrolled) {
+ if (this.isScrollInBottom) {
this.scrollDown();
}
})
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 07899777a1e..5c4926d6ac8 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment;
};
-// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 29fc91733b3..5679b8c9a09 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,6 +2,7 @@
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
+import { pluralize } from './text_utility';
import {
lang,
@@ -143,9 +144,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = '';
if (minutes >= 1) {
- text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else {
- text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 917a45eb06b..a02c79b787e 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
+
+/**
+ * Utility function that calculates GiB of the given bytes.
+ * @param {Number} number
+ * @returns {Number}
+ */
+export function bytesToGiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
+}
+
+/**
+ * Port of rails number_to_human_size
+ * Formats the bytes in number into a more understandable
+ * representation (e.g., giving it 1500 yields 1.5 KB).
+ *
+ * @param {Number} size
+ * @returns {String}
+ */
+export function numberToHumanSize(size) {
+ if (size < BYTES_IN_KIB) {
+ return `${size} bytes`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToKiB(size).toFixed(2)} KiB`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToMiB(size).toFixed(2)} MiB`;
+ }
+ return `${bytesToGiB(size).toFixed(2)} GiB`;
+}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
new file mode 100644
index 00000000000..2dc9cf0cc29
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -0,0 +1,153 @@
+/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+
+const textUtils = {};
+
+textUtils.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+
+textUtils.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+};
+
+textUtils.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+
+textUtils.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
+ }
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+};
+
+textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+ currentLineEmpty = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
+ selectedSplit = selected.split('\n');
+
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
+
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
+ if (blockTag != null && blockTag !== '') {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
+ } else {
+ return "" + tag + val;
+ }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (error) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (error) {}
+ }
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+
+textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ return textArea.setSelectionRange(pos, pos);
+ }
+};
+
+textUtils.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, selected, text;
+ $textArea = $(textArea);
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+
+textUtils.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+};
+
+textUtils.removeListeners = function(form) {
+ return $('.js-md', form).off('click');
+};
+
+textUtils.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+};
+
+export default textUtils;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index f776829f69c..28ab9dddc4c 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,18 +1,13 @@
-/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
-
-import 'vendor/latinise';
-
-var base;
-var w = window;
-if (w.gl == null) {
- w.gl = {};
-}
-if ((base = w.gl).text == null) {
- base.text = {};
-}
-gl.text.addDelimiter = function(text) {
- return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
-};
+/**
+ * Adds a , to a string composed by numbers, at every 3 chars.
+ *
+ * 2333 -> 2,333
+ * 232324 -> 232,324
+ *
+ * @param {String} text
+ * @returns {String}
+ */
+export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
@@ -20,178 +15,43 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count
* @return {Number|String}
*/
-export function highCountTrim(count) {
- return count > 99 ? '99+' : count;
-}
-
-gl.text.randomString = function() {
- return Math.random().toString(36).substring(7);
-};
-gl.text.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-};
-gl.text.getTextWidth = function(text, font) {
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
- *
- * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- */
- // re-use canvas object for better performance
- var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
- var context = canvas.getContext('2d');
- context.font = font;
- return context.measureText(text).width;
-};
-gl.text.selectedText = function(text, textarea) {
- return text.substring(textarea.selectionStart, textarea.selectionEnd);
-};
-gl.text.lineBefore = function(text, textarea) {
- var split;
- split = text.substring(0, textarea.selectionStart).trim().split('\n');
- return split[split.length - 1];
-};
-gl.text.lineAfter = function(text, textarea) {
- return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-};
-gl.text.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
- // To remove the block tag we have to select the line before & after
- if (blockTag != null) {
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
- }
- return selected;
- } else {
- return blockTag + "\n" + selected + "\n" + blockTag;
- }
-};
-gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
-
- // Remove the first newline
- if (selected.indexOf('\n') === 0) {
- removedFirstNewLine = true;
- selected = selected.replace(/\n+/, '');
- }
-
- // Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
- }
-
- selectedSplit = selected.split('\n');
-
- if (!wrap) {
- lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
-
- // Check whether the current line is empty or consists only of spaces(=handle as empty)
- if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
- currentLineEmpty = true;
- }
- }
-
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+export const highCountTrim = count => (count > 99 ? '99+' : count);
- if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
- if (blockTag != null && blockTag !== '') {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
- } else {
- insertText = selectedSplit.map(function(val) {
- if (val.indexOf(tag) === 0) {
- return "" + (val.replace(tag, ''));
- } else {
- return "" + tag + val;
- }
- }).join('\n');
- }
- } else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
- }
+/**
+ * Converst first char to uppercase and replaces undercores with spaces
+ * @param {String} string
+ * @requires {String}
+ */
+export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- if (removedFirstNewLine) {
- insertText = '\n' + insertText;
- }
+/**
+ * Adds an 's' to the end of the string when count is bigger than 0
+ * @param {String} str
+ * @param {Number} count
+ * @returns {String}
+ */
+export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
- if (removedLastNewLine) {
- insertText += '\n';
- }
+/**
+ * Replaces underscores with dashes
+ * @param {*} str
+ * @returns {String}
+ */
+export const dasherize = str => str.replace(/[_\s]+/g, '-');
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-};
-gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
+/**
+ * Removes accents and converts to lower case
+ * @param {String} str
+ * @returns {String}
+ */
+export const slugify = str => str.trim().toLowerCase();
- if (removedLastNewLine) {
- pos -= 1;
- }
+/**
+ * Truncates given text
+ *
+ * @param {String} string
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
- return textArea.setSelectionRange(pos, pos);
- }
-};
-gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, selected, text;
- $textArea = $(textArea);
- textArea = $textArea.get(0);
- text = $textArea.val();
- selected = this.selectedText(text, textArea);
- $textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-};
-gl.text.init = function(form) {
- var self;
- self = this;
- return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
- });
-};
-gl.text.removeListeners = function(form) {
- return $('.js-md', form).off('click');
-};
-gl.text.humanize = function(string) {
- return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
-};
-gl.text.pluralize = function(str, count) {
- return str + (count > 1 || count === 0 ? 's' : '');
-};
-gl.text.truncate = function(string, maxLength) {
- return string.substr(0, (maxLength - 3)) + '...';
-};
-gl.text.dasherize = function(str) {
- return str.replace(/[_\s]+/g, '-');
-};
-gl.text.slugify = function(str) {
- return str.trim().toLowerCase().latinise();
-};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1aa63216baf..17236c91490 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
+export function redirectTo(url) {
+ return window.location.assign(url);
+}
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 31c5cfc5e55..0035dd23011 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
-import './lib/utils/text_utility';
import './lib/utils/url_utility';
// behaviors
@@ -70,8 +69,6 @@ import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
-import './project';
-import './project_avatar';
import './project_find_file';
import './project_import';
import './project_label_subscription';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 6264750a4fb..52315e969d1 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -5,7 +5,6 @@ export default class Members {
}
addListeners() {
- $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
@@ -33,17 +32,6 @@ export default class Members {
});
});
}
- // eslint-disable-next-line class-methods-use-this
- removeRow(e) {
- const $target = $(e.target);
-
- if ($target.hasClass('btn-remove')) {
- $target.closest('.member')
- .fadeOut(function fadeOutMemberRow() {
- $(this).remove();
- });
- }
- }
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index af0658eb668..d30ff12bb59 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
+import { addDelimiter } from './lib/utils/text_utility';
(function() {
this.MergeRequest = (function() {
@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
- $el.text(gl.text.addDelimiter(count));
+ $el.text(addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index db8f85759b2..30e02554b65 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -357,7 +357,8 @@
@click="handleSave(true)"
v-if="canUpdateIssue"
:class="actionButtonClassNames"
- class="btn btn-comment btn-comment-and-close">
+ :disabled="isSubmitting"
+ class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}}
</button>
<button
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 547140b1a43..19d8e1f49cf 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,7 +1,7 @@
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
-
+ import { dasherize } from '../../../lib/utils/text_utility';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
@@ -39,7 +39,7 @@
computed: {
cssClass() {
- const actionIconDash = gl.text.dasherize(this.actionIcon);
+ const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fe6602259e2..ddb78aaeea1 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,139 +1,131 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie';
-(function() {
- this.Project = (function() {
- function Project() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+export default class Project {
+ constructor() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
- const selectedCloneOption = $cloneBtnText.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', (e) => {
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
-
- e.preventDefault();
-
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text($this.text());
-
- return $('.clone').text(url);
- });
- // Ref switcher
- this.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
- });
- $('.hide-no-ssh-message').on('click', function(e) {
- Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
- return e.preventDefault();
- });
- $('.hide-no-password-message').on('click', function(e) {
- Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
- return e.preventDefault();
- });
- this.projectSelectDropdown();
+ const selectedCloneOption = $cloneBtnText.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- Project.prototype.projectSelectDropdown = function() {
- new ProjectSelect();
- $('.project-item-select').on('click', (function(_this) {
- return function(e) {
- return _this.changeProject($(e.currentTarget).val());
- };
- })(this));
- };
-
- Project.prototype.changeProject = function(url) {
- return window.location = url;
- };
-
- Project.prototype.initRefSwitcher = function() {
- var refListItem = document.createElement('li');
- var refLink = document.createElement('a');
-
- refLink.href = '#';
-
- return $('.js-project-refs-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- return $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref'),
- search: term
- },
- dataType: "json"
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- filterByText: true,
- inputFieldName: $dropdown.data('input-field-name'),
- fieldName: $dropdown.data('field-name'),
- renderRow: function(ref) {
- var li = refListItem.cloneNode(false);
-
- if (ref.header != null) {
- li.className = 'dropdown-header';
- li.textContent = ref.header;
- } else {
- var link = refLink.cloneNode(false);
-
- if (ref === selected) {
- link.className = 'is-active';
- }
-
- link.textContent = ref;
- link.dataset.ref = ref;
-
- li.appendChild(link);
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+
+ e.preventDefault();
+
+ $('.is-active', $cloneOptions).not($this).removeClass('is-active');
+ $this.toggleClass('is-active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text($this.text());
+
+ return $('.clone').text(url);
+ });
+ // Ref switcher
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.hide-no-ssh-message').on('click', function(e) {
+ Cookies.set('hide_no_ssh_message', 'false');
+ $(this).parents('.no-ssh-key-message').remove();
+ return e.preventDefault();
+ });
+ $('.hide-no-password-message').on('click', function(e) {
+ Cookies.set('hide_no_password_message', 'false');
+ $(this).parents('.no-password-message').remove();
+ return e.preventDefault();
+ });
+ Project.projectSelectDropdown();
+ }
+
+ static projectSelectDropdown () {
+ new ProjectSelect();
+ $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
+ }
+
+ static changeProject(url) {
+ return window.location = url;
+ }
+
+ static initRefSwitcher() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
+ return $('.js-project-refs-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ dataType: 'json',
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
+ fieldName: $dropdown.data('field-name'),
+ renderRow: function(ref) {
+ var li = refListItem.cloneNode(false);
+
+ if (ref.header != null) {
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
+ } else {
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
}
- return li;
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(options) {
- const { e } = options;
- e.preventDefault();
- if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form');
-
- var $visit = $dropdown.data('visit');
- var shouldVisit = $visit ? true : $visit;
- var action = $form.attr('action');
- var divider = action.indexOf('?') === -1 ? '?' : '&';
- if (shouldVisit) {
- gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
- }
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
+ }
+
+ return li;
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(options) {
+ const { e } = options;
+ e.preventDefault();
+ if ($('input[name="ref"]').length) {
+ var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = $visit ? true : $visit;
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
+ if (shouldVisit) {
+ gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
- });
+ },
});
- };
-
- return Project;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index aabdfbf65e2..56627aa155c 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,20 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.ProjectAvatar = (function() {
- function ProjectAvatar() {
- $('.js-choose-project-avatar-button').bind('click', function() {
- var form;
- form = $(this).closest('form');
- return form.find('.js-project-avatar-input').click();
- });
- $('.js-project-avatar-input').bind('change', function() {
- var filename, form;
- form = $(this).closest('form');
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find('.js-avatar-filename').text(filename);
- });
- }
+export default function projectAvatar() {
+ $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-project-avatar-input').click();
+ });
- return ProjectAvatar;
- })();
-}).call(window);
+ $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 08334bf1ec5..d2d26d6f67e 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,13 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
+import { visitUrl } from './lib/utils/url_utility';
-(function() {
- this.ProjectImport = (function() {
- function ProjectImport() {
- setTimeout(function() {
- return gl.utils.visitUrl(location.href);
- }, 5000);
- }
+export default function projectImport() {
+ setTimeout(() => {
+ visitUrl(location.href);
+ }, 5000);
+}
- return ProjectImport;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index e917279947e..14d43e135fe 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
+ import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
@@ -97,7 +102,7 @@
</span>
</td>
<td>
- {{item.size}}
+ {{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
index be290c268b1..120ce96f44d 100644
--- a/app/assets/javascripts/repo/stores/actions.js
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
-export const redirectToUrl = url => gl.utils.visitUrl(url);
+export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
- redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
+ dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
index b81a70dfd1e..61d9a5af3e3 100644
--- a/app/assets/javascripts/repo/stores/actions/branch.js
+++ b/app/assets/javascripts/repo/stores/actions/branch.js
@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
-export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
- rootState.project.id,
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.project.id,
{
branch,
- ref: rootState.currentBranch,
+ ref: state.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
- const url = location.href.replace(rootState.currentBranch, branchName);
+ const url = location.href.replace(state.currentBranch, branchName);
pushState(url);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 219ff94924e..13e4cb5717e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,5 +1,5 @@
import tooltip from '../../vue_shared/directives/tooltip';
-import '../../lib/utils/text_utility';
+import { pluralize } from '../../lib/utils/text_utility';
export default {
name: 'MRWidgetHeader',
@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
- return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+ return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index a0025ddb598..7a865587444 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { slugify } from './lib/utils/text_utility';
export default class Wikis {
constructor() {
@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = gl.text.slugify(slugInput.value);
+ const slug = slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c334f39f416..66212be1b8f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -34,6 +34,7 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
+@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 6bb096fc5bd..10f9e9b70b0 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -7,29 +7,67 @@
width: 100%;
height: 100%;
padding-bottom: 25px;
- border: 1px solid $border-color;
border-radius: $border-radius-default;
}
}
-.blank-state {
- padding-top: 20px;
- padding-bottom: 20px;
+.blank-state-row {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ height: 100%;
+}
+
+.blank-state-welcome {
text-align: center;
+ padding: 20px 0 40px;
+
+ .blank-state-welcome-title {
+ font-size: 24px;
+ }
+
+ .blank-state-text {
+ margin-bottom: 0;
+ }
+}
- &.blank-state-welcome {
- .blank-state-welcome-title {
- font-size: 24px;
+.blank-state-link {
+ display: block;
+ color: $gl-text-color;
+ flex: 0 0 100%;
+ margin-bottom: 15px;
+
+ @media (min-width: $screen-sm-min) {
+ flex: 0 0 49%;
+
+ &:nth-child(odd) {
+ margin-right: 5px;
}
- .blank-state-text {
- margin-bottom: 0;
+ &:nth-child(even) {
+ margin-left: 5px;
}
}
- .blank-state-icon {
- padding-bottom: 20px;
+ &:hover {
+ background-color: $gray-light;
+ text-decoration: none;
+ color: $gl-text-color;
+ }
+}
+
+.blank-state {
+ padding: 20px;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+ padding: 50px 30px;
+ }
+ .blank-state-icon {
svg {
display: block;
margin: auto;
@@ -38,13 +76,17 @@
.blank-state-title {
margin-top: 0;
- margin-bottom: 10px;
font-size: 18px;
}
- .blank-state-text {
- max-width: $container-text-max-width;
- margin: 0 auto $gl-padding;
- font-size: 14px;
+ .blank-state-body {
+ @media (max-width: $screen-xs-max) {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: 20px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1cdfa904374..f0139b5f33a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -401,10 +401,13 @@
.breadcrumbs-list {
display: -webkit-flex;
display: flex;
- flex-wrap: wrap;
margin-bottom: 0;
line-height: 16px;
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
+
> li {
display: flex;
align-items: center;
@@ -412,24 +415,35 @@
padding: 2px 0;
&:not(:last-child) {
- margin-right: 20px;
+ padding-right: 20px;
+
+ &:not(.dropdown) {
+ overflow: hidden;
+ }
}
> a {
font-size: 12px;
color: currentColor;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 0 1 auto;
}
}
}
.breadcrumb-item-text {
- @include str-truncated(128px);
text-decoration: inherit;
+
+ @media (max-width: $screen-xs-max) {
+ @include str-truncated(128px);
+ }
}
.breadcrumbs-list-angle {
position: absolute;
- right: -12px;
+ right: 7px;
top: 50%;
color: $gl-text-color-tertiary;
transform: translateY(-50%);
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 16d5edde61e..33012133b66 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -180,3 +180,31 @@
display: none;
}
}
+
+@mixin triangle($color, $border-color, $size, $border-size) {
+ &::before,
+ &::after {
+ bottom: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: '';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+
+ &::before {
+ border-color: transparent;
+ border-bottom-color: $border-color;
+ border-width: ($size + $border-size);
+ margin-left: -($size + $border-size);
+ }
+
+ &::after {
+ border-color: transparent;
+ border-bottom-color: $color;
+ border-width: $size;
+ margin-left: -$size;
+ }
+}
diff --git a/app/assets/stylesheets/framework/popup.scss b/app/assets/stylesheets/framework/popup.scss
new file mode 100644
index 00000000000..5c76205095f
--- /dev/null
+++ b/app/assets/stylesheets/framework/popup.scss
@@ -0,0 +1,15 @@
+.popup {
+ @include triangle(
+ $gray-lighter,
+ $gray-darker,
+ $popup-triangle-size,
+ $popup-triangle-border-size
+ );
+
+ padding: $gl-padding;
+ background-color: $gray-lighter;
+ border: 1px solid $gray-darker;
+ border-radius: $border-radius-default;
+ box-shadow: 0 5px 8px $popup-box-shadow-color;
+ position: relative;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8bda344c3de..cb2a237f574 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -719,3 +719,10 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
+
+/*
+Popup
+*/
+$popup-triangle-size: 15px;
+$popup-triangle-border-size: 1px;
+$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 8b9b47a41bc..5d630c7d61e 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -249,3 +249,22 @@
}
}
}
+
+.modal-doorkeepr-auth,
+.doorkeeper-app-form {
+ .scope-description {
+ color: $theme-gray-700;
+ }
+}
+
+.modal-doorkeepr-auth {
+ .modal-body {
+ padding: $gl-padding;
+ }
+}
+
+.doorkeeper-app-form {
+ .scope-description {
+ margin: 0 0 5px 17px;
+ }
+}
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 6cca9f95618..4311f9d4db9 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -92,15 +92,7 @@ module LfsRequest
end
def storage_project
- @storage_project ||= begin
- result = project
-
- # TODO: Make this go to the fork_network root immeadiatly
- # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
- result = result.fork_source while result.forked?
-
- result
- end
+ @storage_project ||= project.lfs_storage_project
end
def objects
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 02c5857eea7..e89eaf7edda 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def redirect_out_of_range(todos)
total_pages =
if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count / todos.limit_value).ceil
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
else
todos.total_pages
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1b985ea9763..1c4c09c772f 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :cancel_all]
+ except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
+ before_action :authorize_erase_build!, only: [:erase]
layout 'project'
@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build)
end
+ def authorize_erase_build!
+ return access_denied! unless can?(current_user, :erase_build, build)
+ end
+
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
index b8f52e31926..c3f5358b577 100644
--- a/app/finders/autocomplete_users_finder.rb
+++ b/app/finders/autocomplete_users_finder.rb
@@ -45,7 +45,7 @@ class AutocompleteUsersFinder
def find_users
return users_from_project if project
- return group.users if group
+ return group.users_with_parents if group
return User.all if current_user
User.none
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 24c07f3dc70..b46ec5e5350 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -36,6 +36,7 @@ class IssuableFinder
iids
label_name
milestone_title
+ my_reaction_emoji
non_archived
project_id
scope
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ec779c1c447..c6a83f21ceb 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -23,10 +23,17 @@ module IconsHelper
render "shared/icons/#{icon_name}.svg", size: size
end
+ def sprite_icon_path
+ # SVG Sprites currently don't work across domains, so in the case of a CDN
+ # we have to set the current path deliberately to prevent addition of asset_host
+ sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
+ ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
+ end
+
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?
- content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
+ content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end
def audit_icon(names, options = {})
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6ca46ae89c1..1b2b0d17910 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -192,6 +192,10 @@ module Ci
project.build_timeout
end
+ def triggered_by?(current_user)
+ user == current_user
+ end
+
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fcbe3d2b67b..19814864e50 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -66,8 +66,8 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition created: :pending
- transition [:success, :failed, :canceled, :skipped] => :running
+ transition [:created, :skipped] => :pending
+ transition [:success, :failed, :canceled] => :running
end
event :run do
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 2ec70203710..10659030910 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -4,15 +4,26 @@ module Avatarable
def avatar_path(only_path: true)
return unless self[:avatar].present?
- # If only_path is true then use the relative path of avatar.
- # Otherwise use full path (including host).
asset_host = ActionController::Base.asset_host
- gitlab_host = only_path ? gitlab_config.relative_url_root : gitlab_config.url
+ use_asset_host = asset_host.present?
- # If asset_host is set then it is expected that assets are handled by a standalone host.
- # That means we do not want to get GitLab's relative_url_root option anymore.
- host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host
+ # Avatars for private and internal groups and projects require authentication to be viewed,
+ # which means they can only be served by Rails, on the regular GitLab host.
+ # If an asset host is configured, we need to return the fully qualified URL
+ # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
+ if use_asset_host && respond_to?(:public?) && !public?
+ use_asset_host = false
+ only_path = false
+ end
- [host, avatar.url].join
+ url_base = ""
+ if use_asset_host
+ url_base << asset_host unless only_path
+ else
+ url_base << gitlab_config.base_url unless only_path
+ url_base << gitlab_config.relative_url_root
+ end
+
+ url_base + avatar.url
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 710fc1ed647..7026f565706 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -86,6 +86,14 @@ module Milestoneish
false
end
+ def total_issue_time_spent
+ @total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
+ end
+
+ def human_total_issue_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
+ end
+
private
def count_issues_by_state(user)
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index d88a92dc027..ae5f138a920 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -18,7 +18,8 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
- before_validation :set_original_position, :update_position, on: :create
+ before_validation :set_original_position, on: :create
+ before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code
after_save :keep_around_commits
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3b3c7fb7f8b..b5abc8f57b0 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -262,10 +262,6 @@ class Issue < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed? || confidential_changed?
- end
-
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b7cf96abe83..fc586fa216e 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- def storage_project(project)
- if project && project.forked?
- storage_project(project.forked_from_project)
- else
- project
- end
- end
-
def project_allowed_access?(project)
- projects.exists?(storage_project(project).id)
+ projects.exists?(project.lfs_storage_project.id)
end
def self.destroy_unreferenced
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index efd8cca2947..f1a5cc73e83 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -865,7 +865,19 @@ class MergeRequest < ActiveRecord::Base
#
def all_commit_shas
if persisted?
- column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha')
+ # 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
+
+ column_shas = MergeRequestDiffCommit
+ .where(merge_request_diff: diffs_relation)
+ .limit(10_000)
+ .pluck('sha')
+
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq
@@ -946,10 +958,6 @@ class MergeRequest < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed?
- end
-
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 2e824cda525..43c77f3f2a2 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -69,6 +69,10 @@ class PagesDomain < ActiveRecord::Base
current < x509.not_before || x509.not_after < current
end
+ def expiration
+ x509&.not_after
+ end
+
def subject
return unless x509
x509.subject.to_s
diff --git a/app/models/project.rb b/app/models/project.rb
index bae16b6b2af..853f6bc504a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1047,6 +1047,18 @@ class Project < ActiveRecord::Base
forked_from_project || fork_network&.root_project
end
+ def lfs_storage_project
+ @lfs_storage_project ||= begin
+ result = self
+
+ # TODO: Make this go to the fork_network root immeadiatly
+ # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ result = result.fork_source while result&.forked?
+
+ result || self
+ end
+ end
+
def personal?
!group
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6bdee538172..3a89fa9264b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1062,6 +1062,10 @@ class Repository
blob_data_at(sha, path)
end
+ def fetch_ref(source_repository, source_ref:, target_ref:)
+ raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 984e5482288..1ab391a5a9d 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -10,6 +10,15 @@ module Ci
end
end
- rule { protected_ref }.prevent :update_build
+ condition(:owner_of_job) do
+ can?(:developer_access) && @subject.triggered_by?(@user)
+ end
+
+ rule { protected_ref }.policy do
+ prevent :update_build
+ prevent :erase_build
+ end
+
+ rule { can?(:master_access) | owner_of_job }.enable :erase_build
end
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 8c89eea607f..69d46f5ec14 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
- expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
+ expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 5f47592e4ad..9d52b8d9752 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity
expose :state
expose :deleted_at
- expose :branch_name
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 120af8c1e61..a9813d774bb 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -1,5 +1,7 @@
module Ci
class PipelineTriggerService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
@@ -26,9 +28,9 @@ module Ci
end
def trigger_from_token
- return @trigger if defined?(@trigger)
-
- @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ strong_memoize(:trigger) do
+ Ci::Trigger.find_by_token(params[:token].to_s)
+ end
end
def create_pipeline_variables!(pipeline)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 68b49d880f7..90865867ff0 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -187,7 +187,7 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
- update_project_counters = issuable.project && issuable.update_project_counter_caches?
+ update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
@@ -288,4 +288,8 @@ class IssuableBaseService < BaseService
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
+
+ def update_project_counter_caches?(issuable)
+ issuable.state_changed?
+ end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 735257c4779..b680eaf5a49 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -45,5 +45,9 @@ module Issues
params.delete(:assignee_ids)
end
end
+
+ def update_project_counter_caches?(issue)
+ super || issue.confidential_changed?
+ end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 156e7b2f078..1da4dbd9e96 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -18,15 +18,7 @@ module MergeRequests
@merge_request = merge_request
- unless @merge_request.mergeable?
- return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
- end
-
- @source = find_merge_source
-
- unless @source
- return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
- end
+ error_check!
merge_request.in_locked_state do
if commit
@@ -41,6 +33,19 @@ module MergeRequests
private
+ def error_check!
+ error =
+ if @merge_request.should_be_rebased?
+ 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ elsif !@merge_request.mergeable?
+ 'Merge request is not mergeable'
+ elsif !source
+ 'No source for merge'
+ end
+
+ raise MergeError, error if error
+ end
+
def commit
message = params[:commit_message] || merge_request.merge_commit_message
@@ -91,8 +96,8 @@ module MergeRequests
merge_request.to_reference(full: true)
end
- def find_merge_source
- merge_request.diff_head_sha
+ def source
+ @source ||= @merge_request.diff_head_sha
end
end
end
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 4965dffab9d..4f60be698e9 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -64,7 +64,7 @@
%th Projects
%th Jobs
%th Tags
- %th Last contact
+ %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
%th
- @runners.each do |runner|
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 57544559824..573a4b93d67 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,33 +1,41 @@
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_user", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Add user
- %p.blank-state-text
- Add your team members and others to GitLab.
- = link_to new_admin_user_path, class: "btn btn-new" do
- New user
+.blank-state-row
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
-.blank-state
- .blank-state-icon
- = custom_icon("configure_server", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Configure GitLab
- %p.blank-state-text
- Make adjustments to how your GitLab instance is set up.
- = link_to admin_root_path, class: "btn btn-new" do
- Configure
+ - if current_user.can_create_group?
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are a great way to organize projects and people.
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group
- %p.blank-state-text
- Groups are a great way to organize projects and people.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+ = link_to new_admin_user_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_user", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Add people
+ %p.blank-state-text
+ Add your team members and others to GitLab.
+
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("configure_server", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Configure GitLab
+ %p.blank-state-text
+ Make adjustments to how your GitLab instance is set up.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index a93a3415ee1..8d5bddbb288 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -1,48 +1,58 @@
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group for several dependent projects.
- %p.blank-state-text
- Groups are the best way to manage projects and members.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+.blank-state-row
+ - if current_user.can_create_project?
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
+ - else
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ If you are added to a project, it will be displayed here.
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_project", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a project
- %p.blank-state-text
- - if current_user.can_create_project?
- You don't have access to any projects right now.
- You can create up to
- %strong= number_with_delimiter(current_user.projects_limit)
- = succeed "." do
- = "project".pluralize(current_user.projects_limit)
- - else
- If you are added to a project, it will be displayed here.
- - if current_user.can_create_project?
- = link_to new_project_path, class: "btn btn-new" do
- New project
+ - if current_user.can_create_group?
+ = link_to new_group_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
-- if public_project_count > 0
- .blank-state
- .blank-state-icon
- = custom_icon("globe", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Explore public projects
- %p.blank-state-text
- There are
- = number_with_delimiter(public_project_count)
- public projects on this server.
- Public projects are an easy way to allow
- everyone to have read-only access.
- = link_to trending_explore_projects_path, class: "btn btn-new" do
- Browse projects
+ - if public_project_count > 0
+ = link_to trending_explore_projects_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("globe", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Explore public projects
+ %p.blank-state-text
+ There are
+ = number_with_delimiter(public_project_count)
+ public projects on this server.
+ Public projects are an easy way to allow
+ everyone to have read-only access.
+
+ = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("lightbulb", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Learn more about GitLab
+ %p.blank-state-text
+ Take a look at the documentation to discover all of GitLab's capabilities.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index ad3fac6d164..18a82feb189 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,12 +1,13 @@
-.row.blank-state-parent-container
+.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
- .blank-state.blank-state-welcome
- %h2.blank-state-welcome-title
- Welcome to GitLab
- %p.blank-state-text
- Code, test, and deploy together
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
+ .row
+ .blank-state-welcome
+ %h2.blank-state-welcome-title
+ Welcome to GitLab
+ %p.blank-state-text
+ Code, test, and deploy together
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index b3313c7c985..cf0e0de1ca4 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(application)
.form-group
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 8ba88906714..6d9c6b5572a 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,5 +1,5 @@
%main{ :role => "main" }
- .modal-no-backdrop
+ .modal-no-backdrop.modal-doorkeepr-auth
.modal-content
.modal-header
%h3.page-title
@@ -16,14 +16,26 @@
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p
- You are about to authorize
+ An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
- to use your account.
- - if @pre_auth.scopes
+ is requesting access to your GitLab account.
+
+ - auth_app_owner = @pre_auth.client.application.owner
+ - if auth_app_owner
+ This application was created by
+ = succeed "." do
+ = link_to auth_app_owner.name, user_path(auth_app_owner)
+
+ Please note that this application is not provided by GitLab and you should verify its authenticity before
+ allowing access.
+ - if @pre_auth.scopes
+ %p
This application will be able to:
%ul
- @pre_auth.scopes.each do |scope|
- %li= t scope, scope: [:doorkeeper, :scopes]
+ %li
+ %strong= t scope, scope: [:doorkeeper, :scopes]
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index a80518f7986..3e36da31ea3 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -1,10 +1,15 @@
- discussion = @note.discussion if @note.part_of_discussion?
+- diff_discussion = discussion&.diff_discussion?
+- on_image = discussion.on_image? if diff_discussion
+
- if discussion
+ - phrase_end_char = on_image ? "." : ":"
+
%p.details
- = succeed ':' do
+ = succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author)
- - if discussion.diff_discussion?
+ - if diff_discussion
- if discussion.new_discussion?
started a new discussion
- else
@@ -21,7 +26,7 @@
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
-- if discussion&.diff_discussion?
+- if diff_discussion && !on_image
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index ce0e3872240..2abd2c9e652 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -71,7 +71,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- - if can?(current_user, :update_build, @project) && @build.erasable?
+ - if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
index ea91e8af70e..f53b81cada6 100644
--- a/app/views/projects/protected_tags/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -2,7 +2,7 @@
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
- dropdown_class: 'dropdown-menu-selectable',
+ dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag'
diff --git a/app/views/shared/icons/_add_new_project.svg b/app/views/shared/icons/_add_new_project.svg
index 3c1e15453df..cf8762944ca 100644
--- a/app/views/shared/icons/_add_new_project.svg
+++ b/app/views/shared/icons/_add_new_project.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_hourglass.svg b/app/views/shared/icons/_icon_hourglass.svg
new file mode 100644
index 00000000000..fe7e497ce13
--- /dev/null
+++ b/app/views/shared/icons/_icon_hourglass.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
diff --git a/app/views/shared/icons/_lightbulb.svg b/app/views/shared/icons/_lightbulb.svg
new file mode 100644
index 00000000000..2fcc4c65f99
--- /dev/null
+++ b/app/views/shared/icons/_lightbulb.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 951b4dd7b36..2c27dd638a7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -104,7 +104,6 @@
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
- remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index f03e0ab154c..4f51455c26e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -85,6 +85,22 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
+ .block.time_spent
+ .sidebar-collapsed-icon
+ = custom_icon('icon_hourglass')
+ %span.collapsed-milestone-total-time-spent
+ - if milestone.human_total_issue_time_spent
+ = milestone.human_total_issue_time_spent
+ - else
+ = _("None")
+ .title.hide-collapsed
+ = _("Total issue time spent")
+ .value.hide-collapsed
+ - if milestone.human_total_issue_time_spent
+ %span.bold= milestone.human_total_issue_time_spent
+ - else
+ %span.no-value= _("No time spent")
+
.block.merge-requests
.sidebar-collapsed-icon
%strong
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 8bbaf431536..ae437dd16d6 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -7,3 +7,4 @@
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes])
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 150788ca611..89ae17cef37 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -2,10 +2,6 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def metrics_tags
- @metrics_tags || {}
- end
-
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
@@ -13,11 +9,6 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
- @metrics_tags = {
- project_id: project_id,
- user_id: user_id
- }
-
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end
end
diff --git a/changelogs/unreleased/.yml b/changelogs/unreleased/.yml
new file mode 100644
index 00000000000..acf0bb80c72
--- /dev/null
+++ b/changelogs/unreleased/.yml
@@ -0,0 +1,5 @@
+---
+title: Remove update merge request worker tagging.
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/3615-improve-welcome-screen.yml b/changelogs/unreleased/3615-improve-welcome-screen.yml
new file mode 100644
index 00000000000..862efddb162
--- /dev/null
+++ b/changelogs/unreleased/3615-improve-welcome-screen.yml
@@ -0,0 +1,5 @@
+---
+title: Reorganize welcome page for new users
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
new file mode 100644
index 00000000000..b5342d4adf8
--- /dev/null
+++ b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
+merge_request: 14607
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml b/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml
deleted file mode 100644
index c7e840f0723..00000000000
--- a/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix GPG signature popup info in Safari and Firefox
-merge_request: 15228
-author:
-type: fixed
diff --git a/changelogs/unreleased/39109-reenable-scroll-job.yml b/changelogs/unreleased/39109-reenable-scroll-job.yml
new file mode 100644
index 00000000000..a771f8f8941
--- /dev/null
+++ b/changelogs/unreleased/39109-reenable-scroll-job.yml
@@ -0,0 +1,5 @@
+---
+title: Enables scroll to bottom once user has scrolled back to bottom in job log
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/39335-add-time-spend-to-milestones.yml b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
new file mode 100644
index 00000000000..41a43418cbf
--- /dev/null
+++ b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Add total time spent to milestones
+merge_request: 15116
+author: George Andrinopoulos
+type: added
diff --git a/changelogs/unreleased/39436-pages-api-administrative.yml b/changelogs/unreleased/39436-pages-api-administrative.yml
new file mode 100644
index 00000000000..f38bbbd479c
--- /dev/null
+++ b/changelogs/unreleased/39436-pages-api-administrative.yml
@@ -0,0 +1,5 @@
+---
+title: Add administrative endpoint to list all pages domains
+merge_request: 15160
+author: Travis Miller
+type: added
diff --git a/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
new file mode 100644
index 00000000000..056afe43010
--- /dev/null
+++ b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Move update_project_counter_caches? out of issue and merge request
+merge_request: 15300
+author: George Andrinopoulos
+type: other
diff --git a/changelogs/unreleased/39704_fix_webhooks_log_time.yml b/changelogs/unreleased/39704_fix_webhooks_log_time.yml
deleted file mode 100644
index 1234663e66b..00000000000
--- a/changelogs/unreleased/39704_fix_webhooks_log_time.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix webhooks recent deliveries
-merge_request: 15146
-author: Alexander Randa (@randaalex)
-type: fixed
diff --git a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
new file mode 100644
index 00000000000..580b97241e7
--- /dev/null
+++ b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
@@ -0,0 +1,6 @@
+---
+title: Fix pipeline status transition for single manual job. This would also fix pipeline
+ duration becuse it is depending on status transition
+merge_request: 15251
+author:
+type: fixed
diff --git a/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
new file mode 100644
index 00000000000..358c007387e
--- /dev/null
+++ b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix acceptance of username for Mattermost service update
+merge_request: 15275
+author:
+type: fixed
diff --git a/changelogs/unreleased/40068-runner-sorting-regression.yml b/changelogs/unreleased/40068-runner-sorting-regression.yml
new file mode 100644
index 00000000000..6a2bd59d6d6
--- /dev/null
+++ b/changelogs/unreleased/40068-runner-sorting-regression.yml
@@ -0,0 +1,5 @@
+---
+title: Revert a regression on runners sorting (!15134)
+merge_request: 15341
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/40092-fix-cluster-size.yml b/changelogs/unreleased/40092-fix-cluster-size.yml
new file mode 100644
index 00000000000..e7a16c6920c
--- /dev/null
+++ b/changelogs/unreleased/40092-fix-cluster-size.yml
@@ -0,0 +1,5 @@
+---
+title: Formats bytes to human reabale number in registry table
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-typescript.yml b/changelogs/unreleased/add-typescript.yml
deleted file mode 100644
index a048f240855..00000000000
--- a/changelogs/unreleased/add-typescript.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds typescript support
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml b/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml
new file mode 100644
index 00000000000..ae1eb3aedd5
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-network-memberships-for-deleted-source.yml
@@ -0,0 +1,5 @@
+---
+title: Don't try to create fork network memberships for forks with a missing source
+merge_request: 15366
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml b/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml
new file mode 100644
index 00000000000..48b4051711c
--- /dev/null
+++ b/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml
@@ -0,0 +1,5 @@
+---
+title: Don't use JS to delete memberships from projects and groups
+merge_request: 15344
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-unlink-fixes.yml b/changelogs/unreleased/bvl-unlink-fixes.yml
deleted file mode 100644
index 685d78f479d..00000000000
--- a/changelogs/unreleased/bvl-unlink-fixes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix issues with forked projects of which the source was deleted
-merge_request: 15150
-author:
-type: fixed
diff --git a/changelogs/unreleased/cleanup-issues-schema.yml b/changelogs/unreleased/cleanup-issues-schema.yml
new file mode 100644
index 00000000000..9f5fb0bdf82
--- /dev/null
+++ b/changelogs/unreleased/cleanup-issues-schema.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up schema of the "issues" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/dm-authorize-admin-oauth-application.yml b/changelogs/unreleased/dm-authorize-admin-oauth-application.yml
new file mode 100644
index 00000000000..2787485bc28
--- /dev/null
+++ b/changelogs/unreleased/dm-authorize-admin-oauth-application.yml
@@ -0,0 +1,6 @@
+---
+title: Prevent error when authorizing an admin-created OAauth application without
+ a set owner
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-avatarable-with-asset-host.yml b/changelogs/unreleased/dm-avatarable-with-asset-host.yml
new file mode 100644
index 00000000000..6cf8d719afb
--- /dev/null
+++ b/changelogs/unreleased/dm-avatarable-with-asset-host.yml
@@ -0,0 +1,6 @@
+---
+title: Always return full avatar URL for private/internal groups/projects when asset
+ host is set
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml b/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml
deleted file mode 100644
index 42bcf9b1edd..00000000000
--- a/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Make sure group and project creation is blocked for new users that are external
- by default
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml b/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml
new file mode 100644
index 00000000000..32cdfba4eec
--- /dev/null
+++ b/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml
@@ -0,0 +1,6 @@
+---
+title: Ensure merge requests with lots of version don't time out when searching for
+ pipelines
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-filter-by-my-reaction.yml b/changelogs/unreleased/fix-filter-by-my-reaction.yml
new file mode 100644
index 00000000000..8bf91ddf893
--- /dev/null
+++ b/changelogs/unreleased/fix-filter-by-my-reaction.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filter by my reaction is not working
+merge_request: 15345
+author: Hiroyuki Sato
+type: fixed
diff --git a/changelogs/unreleased/fix-import-export-arguments.yml b/changelogs/unreleased/fix-import-export-arguments.yml
deleted file mode 100644
index eee87e313ea..00000000000
--- a/changelogs/unreleased/fix-import-export-arguments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix arguments Import/Export error importing project merge requests
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-mysql-grant-check.yml b/changelogs/unreleased/fix-mysql-grant-check.yml
deleted file mode 100644
index a1c1aa67d79..00000000000
--- a/changelogs/unreleased/fix-mysql-grant-check.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix TRIGGER checks for MySQL
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
new file mode 100644
index 00000000000..198116f34aa
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
@@ -0,0 +1,5 @@
+---
+title: Only owner or master can erase jobs
+merge_request: 15216
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-subgroup-autocomplete.yml b/changelogs/unreleased/fix-subgroup-autocomplete.yml
new file mode 100644
index 00000000000..4baa2b02f77
--- /dev/null
+++ b/changelogs/unreleased/fix-subgroup-autocomplete.yml
@@ -0,0 +1,5 @@
+---
+title: Fix user autocomplete in subgroups
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-todos-last-page.yml b/changelogs/unreleased/fix-todos-last-page.yml
new file mode 100644
index 00000000000..efcdbb75e6e
--- /dev/null
+++ b/changelogs/unreleased/fix-todos-last-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix access to the final page of todos
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_diff_parsing.yml b/changelogs/unreleased/fix_diff_parsing.yml
deleted file mode 100644
index 7a26b4f9ff5..00000000000
--- a/changelogs/unreleased/fix_diff_parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix diff parser so it tolerates to diff special markers in the content
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml
deleted file mode 100644
index a1685497331..00000000000
--- a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/issue_39176.yml b/changelogs/unreleased/issue_39176.yml
deleted file mode 100644
index 6255b51c094..00000000000
--- a/changelogs/unreleased/issue_39176.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render 404 when polling commit notes without having permissions
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/issue_39238.yml b/changelogs/unreleased/issue_39238.yml
new file mode 100644
index 00000000000..75a4969ca9e
--- /dev/null
+++ b/changelogs/unreleased/issue_39238.yml
@@ -0,0 +1,5 @@
+---
+title: Fix image diff notification email from showing wrong content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_40058.yml b/changelogs/unreleased/issue_40058.yml
new file mode 100644
index 00000000000..46e83d947ba
--- /dev/null
+++ b/changelogs/unreleased/issue_40058.yml
@@ -0,0 +1,5 @@
+---
+title: Prevents position update for image diff notes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml
deleted file mode 100644
index 0205d9626b1..00000000000
--- a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix cancel button not working while uploading on the new issue page
-merge_request: 15137
-author:
-type: fixed
diff --git a/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml
deleted file mode 100644
index 556d7d069d3..00000000000
--- a/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove Filesystem check metrics that use too much CPU to handle requests
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-environment-slug-generation.yml b/changelogs/unreleased/sh-fix-environment-slug-generation.yml
deleted file mode 100644
index 8a9c670c52c..00000000000
--- a/changelogs/unreleased/sh-fix-environment-slug-generation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid regenerating the ref path for the environment
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/text-utils.yml b/changelogs/unreleased/text-utils.yml
new file mode 100644
index 00000000000..b95bb82fe01
--- /dev/null
+++ b/changelogs/unreleased/text-utils.yml
@@ -0,0 +1,5 @@
+---
+title: Export text utils functions as es6 module and add tests
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/winh-subgroups-api.yml b/changelogs/unreleased/winh-subgroups-api.yml
new file mode 100644
index 00000000000..c49e3621e9c
--- /dev/null
+++ b/changelogs/unreleased/winh-subgroups-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add /groups/:id/subgroups endpoint to API
+merge_request: 15142
+author: marbemac
+type: added
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 0da6b14c29e..b1c71095d4f 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -62,7 +62,15 @@ en:
read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
-
+ scope_desc:
+ api:
+ Full access to GitLab as the user, including read/write on all their groups and projects
+ read_user:
+ Read-only access to the user's profile information, like username, public email and full name
+ openid:
+ The ability to authenticate using GitLab, and read-only access to the user's profile information
+ sudo:
+ Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
flash:
applications:
create:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 67d7cae3ccf..f7a7182a627 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -108,10 +108,6 @@ var config = {
loader: 'vue-loader',
},
{
- test: /\.ts$/,
- loader: 'ts-loader',
- },
- {
test: /\.svg$/,
loader: 'raw-loader',
},
@@ -256,7 +252,7 @@ var config = {
],
resolve: {
- extensions: ['.js', '.ts'],
+ extensions: ['.js'],
alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
diff --git a/db/migrate/20171106132212_issues_confidential_not_null.rb b/db/migrate/20171106132212_issues_confidential_not_null.rb
new file mode 100644
index 00000000000..c959d2dd938
--- /dev/null
+++ b/db/migrate/20171106132212_issues_confidential_not_null.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesConfidentialNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ end
+
+ def up
+ Issue.where('confidential IS NULL').update_all(confidential: false)
+
+ change_column_null :issues, :confidential, false
+ end
+
+ def down
+ # There's no way / point to revert this.
+ end
+end
diff --git a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
new file mode 100644
index 00000000000..e6a780d0964
--- /dev/null
+++ b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMilestoneIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_milestones
+ where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_milestones.each_batch(of: 100) do |batch|
+ batch.update_all(milestone_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :milestones,
+ column: :milestone_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :milestone_id)
+ end
+end
diff --git a/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
new file mode 100644
index 00000000000..3b8844d7d9f
--- /dev/null
+++ b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesUpdatedByIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_updaters
+ where('NOT EXISTS (SELECT true FROM users WHERE users.id = issues.updated_by_id)')
+ .where('updated_by_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_updaters.each_batch(of: 100) do |batch|
+ batch.update_all(updated_by_id: nil)
+ end
+
+ # This index is only used for foreign keys, and those in turn will always
+ # specify a value. As such we can add a WHERE condition to make the index
+ # smaller.
+ add_concurrent_index(:issues, :updated_by_id, where: 'updated_by_id IS NOT NULL')
+
+ add_concurrent_foreign_key(
+ :issues,
+ :users,
+ column: :updated_by_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :updated_by_id)
+ remove_concurrent_index(:issues, :updated_by_id)
+ end
+end
diff --git a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
new file mode 100644
index 00000000000..8d2ceb8cc18
--- /dev/null
+++ b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
@@ -0,0 +1,44 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMovedToIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_moved_to_issues
+ where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
+ .where('moved_to_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_moved_to_issues.each_batch(of: 100) do |batch|
+ batch.update_all(moved_to_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :issues,
+ column: :moved_to_id,
+ on_delete: :nullify
+ )
+
+ # We're using a partial index here so we only index the data we actually
+ # care about.
+ add_concurrent_index(:issues, :moved_to_id, where: 'moved_to_id IS NOT NULL')
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :moved_to_id)
+ remove_concurrent_index(:issues, :moved_to_id)
+ end
+end
diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb
new file mode 100644
index 00000000000..3d08225c96d
--- /dev/null
+++ b/db/migrate/20171106154015_remove_issues_branch_name.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveIssuesBranchName < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ remove_column :issues, :branch_name, :string
+ end
+end
diff --git a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
new file mode 100644
index 00000000000..e4bed778695
--- /dev/null
+++ b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ NEW_INDEX_NAME = 'idx_issues_on_project_id_and_due_date_and_id_and_state_partial'
+ OLD_INDEX_NAME = 'index_issues_on_project_id_and_due_date_and_id_and_state'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ where: 'due_date IS NOT NULL',
+ name: NEW_INDEX_NAME
+ )
+
+ # We set the column name to nil as otherwise Rails will ignore the custom
+ # index name and remove the wrong index.
+ remove_concurrent_index(:issues, nil, name: OLD_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ name: OLD_INDEX_NAME
+ )
+
+ remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..ad540b1e509
--- /dev/null
+++ b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_column_type_concurrently(:issues, :closed_at, :datetime_with_timezone)
+ end
+
+ def down
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+end
diff --git a/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..88dd8f89ba6
--- /dev/null
+++ b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupAddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+
+ # rubocop:disable Migration/Datetime
+ def down
+ change_column_type_concurrently(:issues, :closed_at, :datetime)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c60cb729b75..37e08d453c8 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: 20171106101200) do
+ActiveRecord::Schema.define(version: 20171106180641) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -817,13 +817,12 @@ ActiveRecord::Schema.define(version: 20171106101200) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.boolean "confidential", default: false, null: false
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
@@ -832,11 +831,11 @@ ActiveRecord::Schema.define(version: 20171106101200) do
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
- t.datetime "closed_at"
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
+ t.datetime_with_timezone "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -845,13 +844,15 @@ ActiveRecord::Schema.define(version: 20171106101200) do
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree
- add_index "issues", ["project_id", "due_date", "id", "state"], name: "index_issues_on_project_id_and_due_date_and_id_and_state", using: :btree
+ add_index "issues", ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree
add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "keys", force: :cascade do |t|
t.integer "user_id"
@@ -1937,8 +1938,11 @@ ActiveRecord::Schema.define(version: 20171106101200) do
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
+ add_foreign_key "issues", "issues", column: "moved_to_id", name: "fk_a194299be1", on_delete: :nullify
+ add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
+ add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 16db9c2f259..6a6e94195a7 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -9,13 +9,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `skip_groups` | array of integers | no | Skip the group IDs passes |
-| `all_available` | boolean | no | Show all the groups you have access to |
-| `search` | string | no | Return list of authorized groups matching the search criteria |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
-| `owned` | boolean | no | Limit by groups owned by the current user |
+| `owned` | boolean | no | Limit to groups owned by the current user |
```
GET /groups
@@ -80,6 +80,47 @@ You can filter by [custom attributes](custom_attributes.md) with:
GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
+## List a groups's subgroups
+
+Get a list of visible direct subgroups in this group.
+When accessed without authentication, only public groups are returned.
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
+| `owned` | boolean | no | Limit to groups owned by the current user |
+
+```
+GET /groups/:id/subgroups
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group",
+ "visibility": "public",
+ "lfs_enabled": true,
+ "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://gitlab.example.com/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": 123
+ }
+]
+```
+
## List a group's projects
Get a list of projects in this group. When accessed without authentication, only
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 51962595e33..50685f335f7 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
+## List all pages domains
+
+Get a list of all pages domains. The user must have admin permissions.
+
+```http
+GET /pages/domains
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
+```
+
+```json
+[
+ {
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "expired": false,
+ "expiration": "2020-04-12T14:32:00.000Z"
+ }
+ }
+]
+```
+
## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
diff --git a/doc/api/services.md b/doc/api/services.md
index e642ec964de..08df26db3ec 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Kubernetes
+
+Kubernetes / Openshift integration
+
+### Create/Edit Kubernetes service
+
+Set Kubernetes service for a project.
+
+```
+PUT /projects/:id/services/kubernetes
+```
+
+Parameters:
+
+- `namespace` (**required**) - The Kubernetes namespace to use
+- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
+- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
+- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
+
+### Delete Kubernetes service
+
+Delete Kubernetes service for a project.
+
+```
+DELETE /projects/:id/services/kubernetes
+```
+
+### Get Kubernetes service settings
+
+Get Kubernetes service settings for a project.
+
+```
+GET /projects/:id/services/kubernetes
+```
+
## Slack slash commands
Ability to receive slash commands from a Slack chat instance.
@@ -572,7 +607,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
-
+| `username` | string | no | The username to use to post the message |
### Delete Mattermost slash command service
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 899be9eae4b..ba82babb38a 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.
+### Assets
+
+#### gitlab-svgs
+
+Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
+
---
[Return to Development documentation](README.md)
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2a004152d5e..4efe911b778 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-1-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab
-**Note:** You can change `10-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index c6a91c8d5c2..076fbf6f710 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -23,6 +23,10 @@ A Microsoft-based [directory service](https://msdn.microsoft.com/en-us/library/b
Building and [delivering software](http://agilemethodology.org/) in phases/parts rather than trying to build everything at once then delivering to the user/client. The latter is known as the WaterFall model.
+### Amazon RDS
+
+External reference: <http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html>
+
### Application Lifecycle Management (ALM)
The entire product lifecycle management process for an application, from requirements management, development, and testing until deployment. GitLab has [advantages](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit#slide=id.g72f2e4906_2_288) over both legacy and modern ALM tools.
@@ -68,7 +72,7 @@ A branch is a parallel version of a repository. This allows you to work on the r
Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
-### Job triggers
+### Job triggers (Build Triggers)
These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) job triggers.
### CEPH
@@ -109,15 +113,15 @@ Atlassian's product for collaboration on documents and projects.
### Continuous Delivery
-A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually.
+A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually. [Amazon moves toward continuous delivery](https://www.youtube.com/watch?v=esEFaY0FDKc)
### Continuous Deployment
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all. [The difference between Continuous Delivery and Continuous Integration.](https://www.youtube.com/watch?v=igwFj8PPSnw)
### Continuous Integration
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day. [Thoughtworks discusses continuous integration.](https://www.thoughtworks.com/continuous-integration)
### Contributor
@@ -127,6 +131,10 @@ Term used for a person contributing to an open source project.
A [natural evolution](https://about.gitlab.com/2016/09/14/gitlab-live-event-recap/) of software development that carries a conversation across functional groups throughout the development process, enabling developers to track the full path of development in a cohesive and intuitive way. ConvDev accelerates the development lifecycle by fostering collaboration and knowledge sharing from idea to production.
+### Cycle Analytics
+
+See <https://gitlab.com/gitlab-org/gitlab-ce/issues/22458>
+
### Cycle Time
The time it takes to move from [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab).
@@ -135,6 +143,10 @@ The time it takes to move from [idea to production](https://about.gitlab.com/201
Atlassian product for High Availability.
+### Dependencies
+
+As in "specify [dependencies](https://gitlab.com/gitlab-org/gitlab-ce/issues/14728) between stages."
+
### Deploy Keys
A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)stored on your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
@@ -151,15 +163,17 @@ The intersection of software engineering, quality assurance, and technology oper
The difference between two commits, or saved changes. This will also be shown visually after the changes.
-#### Directory
+### Directory
A folder used for storing multiple files.
### Docker Container Registry
-A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of GitLab projects. Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of [GitLab projects](https://about.gitlab.com/2016/05/23/gitlab-container-registry/). Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
-### Dynamic Environment
+### Dynamic Environment (review apps)
+
+### EC2 Instance
### ElasticSearch
@@ -167,10 +181,26 @@ Elasticsearch is a flexible, scalable and powerful search service. When [enabled
### Emacs
+External reference: <https://www.masteringemacs.org/article/mastering-key-bindings-emacs>
+
+### First Byte
+
+External reference: <https://en.wikipedia.org/wiki/Time_To_First_Byte>
+
+First Byte (sometimes referred to as time to first byte or [TTFB](https://en.wikipedia.org/wiki/Time_To_First_Byte)) measures the time between making a request and receiving the first byte of information in return. As a result, First Byte encompasses everything that is the backend as well as network transit issues. It differs from [_Speed Index_](#speed-index) mostly by frontend related issues which are included in Speed Index such as javascript loading, page rendering, and so on.
+
### Fork
Your [own copy](https://docs.gitlab.com/ce/workflow/forking_workflow.html) of a repository that allows you to make changes to the repository without affecting the original.
+### Funnel, or: TOFU, MOFU, BOFU
+
+External reference: [Blog post](https://www.weidert.com/whole_brain_marketing_blog/bid/113688/ToFu-MoFu-BoFu-Serving-Up-The-Right-Content-for-Lead-Nurturing)
+
+TOFU: top of funnel
+MOFU: middle of funnel
+BOFU: bottom of funnel
+
### Gerrit
A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
@@ -183,6 +213,8 @@ A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text
[Scripts](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) you can use to trigger actions at certain points.
+Difference between a [webhook](#webhooks) and a git hook: a git hook is local to its repo (usually) while a webhook is not (it can make API or http calls). So for example if you want your linter to fire before you commit, you can set that up with a git hook. If the linter fails, the commit does not go through. A git hook _can_ be configured to go beyond its repo, e.g. by having it make an API call.
+
### GitHost.io
A single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for installing, updating, hosting, and backing up customers' own private and secure GitLab instance.
@@ -211,9 +243,20 @@ Our free SaaS for public and private repositories.
Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+### GitLab High Availability
+
+### GitLab Master Plan
+
+Related blog post: <https://about.gitlab.com/2016/09/13/gitlab-master-plan/>.
+
### GitLab Pages
+
These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
+### GitLab Runner
+
+Related project: <https://gitlab.com/gitlab-org/gitlab-runner>
+
### Gitolite
An [access layer](https://git-scm.com/book/en/v1/Git-on-the-Server-Gitolite) that sits on top of Git. Users are granted access to repos via a simple config file. As an admin, you only need the users' public SSH key and a username.
@@ -226,6 +269,10 @@ A web-based hosting service for projects using Git. It was acquired by GitLab an
An open source programming [language](https://golang.org/).
+### Gogs
+
+External reference: <https://gogs.io/>
+
### GUI/ Git GUI
A portable [graphical interface](https://git-scm.com/docs/git-gui) to Git that allows users to make changes to their repository by making new commits, amending existing ones, creating branches, performing local merges, and fetching/pushing to remote repositories.
@@ -256,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
@@ -290,6 +337,10 @@ GitLab [integrates](https://docs.gitlab.com/ce/administration/auth/ldap.html) wi
Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+### Lint
+
+Static code analysis for our various file types. For example, we use [scss-lint](https://github.com/brigade/scss-lint) to ensure that a consistent code styling is respected. Similar tools: rubocop / eslint.
+
### Load Balancer
A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
@@ -330,6 +381,10 @@ Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-m
[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
+#### Merge Request
+
+[Takes changes](https://docs.gitlab.com/ce/gitlab-basics/add-merge-request.html) from one branch, and applies them into another branch.
+
### Meteor
A [platform](https://www.meteor.com) for building javascript apps.
@@ -350,6 +405,14 @@ A type of software license. It lets people do anything with your code with prope
A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
+#### Mount
+
+External reference:
+
+As stated on the [wikipedia page](https://en.wikipedia.org/wiki/Mount_(Unix)), "Mounting makes file systems, files, directories, devices and special files available for use and available to the user."
+
+For example, we have NFS servers where the _git files_ reside. In order for a worker node to "see" or "use" the git files, the NFS server needs to be _mounted_ on the worker; that is, the worker needs to know that the NFS server exists and how to connect to it. Think of it as getting a shared drive to show up in your Finder (on Mac) or Explorer (on Windows).
+
### MySQL
A relational [database](http://www.mysql.com/) owned by Oracle. Currently only supported if you are using EE.
@@ -360,7 +423,7 @@ A set of symbols that are used to organize objects of various kinds so that thes
### Nginx
-A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). [It can act]((https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md) as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
### OAuth
@@ -380,7 +443,11 @@ GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-cor
### Open Source Software
-Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/). Including to providing access to the source code, open source software must comply with a number of criteria, among them free distribution and no discrimination against persons, groups, or fields of endeavor.
+
+#### Open Source Stewardship
+
+[Related blog post](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### Owner
@@ -430,6 +497,8 @@ A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
+### Raketasks
+
### RE Read Only
Permissions to see a file and its contents, but not change it.
@@ -438,10 +507,24 @@ Permissions to see a file and its contents, but not change it.
In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
+### Regression
+
+A regression is something that used to work one way in the last release and then we made a **breaking change** and it no longer works the same way.
+
+_or_
+
+A regression is defined as a change that results in a negative impact on the functionality of an existing feature due to recent changes, i.e. the latest release.
+
+### Remote mirroring
+
### (Git) Repository
A directory where Git [has been initiatlized](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) to start version controlling your files. The history of your work is stored here. A remote repository is not on your machine, but usually online (like on GitLab.com, for instance). The main remote repository is usually called "Origin."
+##### Remote repository
+
+A [repository](https://about.gitlab.com/2015/05/18/simple-words-for-a-gitlab-newbie/) that is not-on-your-machine, so it's anything that is not your computer. Usually, it is online, GitLab.com for instance. The main remote repository is usually called “Origin”.
+
### Requirements management
Gives your distributed teams a single shared repository to collaborate and share requirements, understand their relationship to tests, and evaluate linked defects. It includes multiple, preconfigured requirement types.
@@ -474,7 +557,11 @@ Software that is hosted centrally and accessed on-demand (i.e. whenever you want
This term is often used by people when they mean "Version Control."
-## Scrum
+#### SCLAU
+
+Abbreviation for SQO Count [Large And Up](https://about.gitlab.com/handbook/sales/#market-segmentation). This is the number of opportunities in large and strategic organizations passed from marketing to sales.
+
+### Scrum
An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
@@ -484,7 +571,9 @@ The board used to track the status and progress of each of the sprint backlog it
### Shell
-Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+
+### Shell command runner
### Single-tenant
@@ -494,6 +583,8 @@ The tenant purchases their own copy of the software and the software can be cust
Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
+### Slash commands
+
### Slave Servers
Also known as secondary servers, these help to spread the load over multiple machines. They also provide backups when the master/primary server crashes.
@@ -502,6 +593,10 @@ Also known as secondary servers, these help to spread the load over multiple mac
Program code as typed by a computer programmer (i.e. it has not yet been compiled/translated by the computer to machine language).
+### Speed Index
+
+[Speed Index](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index) is "the average time at which visible parts of the page are displayed".
+
### SSH Key
A unique identifier of a computer. It is used to identify computers without the need for a password (e.g., On GitLab I have [added the ssh key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html) of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.)
@@ -542,6 +637,16 @@ An open source version control system. Read about [migrating from SVN](https://d
[Represents](https://docs.gitlab.com/ce/api/tags.html) a version of a particular branch at a moment in time.
+### Tenancy
+
+#### Multi-tenant
+
+A [multi-tenant](http://whatis.techtarget.com/definition/multi-tenancy) GitLab instance can have any number of customers - such as companies or groups of users using it. GitLab.com is an example of a multi-tenant GitLab instance.
+
+#### Single-tenant
+
+A [single-tenant](http://searchcloudapplications.techtarget.com/definition/single-tenancy) GitLab instance has only one customer - such as a company - using it. On premise GitLab instances are almost exclusively single-tenant.
+
### Tool Stack
The set of tools used in a process to achieve a common outcome (e.g. set of tools used in Application Lifecycle Management).
@@ -550,9 +655,17 @@ The set of tools used in a process to achieve a common outcome (e.g. set of tool
An open source project management and bug tracking web [application](https://trac.edgewall.org/).
+### True-Up licensing model
+
+### Ubuntu
+
### Untracked files
-New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously. Add them by using the command "git add [file path]"
+
+### Upstream repository vs. GitLab repository
+
+[External conversation](https://news.ycombinator.com/item?id=12487112)
### User
@@ -560,11 +673,11 @@ Anyone interacting with the software.
### Version Control Software (VCS)
-Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present [distributed version control systems](https://en.wikipedia.org/wiki/Distributed_version_control) like Git, Mercurial, Bazaar, and Darcs. If any server dies, and these systems were collaborating via it, any of the client repositories can be copied back up to the server to restore it.
### Virtual Private Cloud (VPC)
-An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
+A [VPC](https://docs.gitlab.com/ce/university/glossary/README.html#virtual-private-cloud-vpc) is an on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
### Virtual private server (VPS)
@@ -572,7 +685,7 @@ A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold a
### VM Instance
-In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any [object](https://cloud.google.com/compute/docs/instances/). An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
### Waterfall
@@ -586,6 +699,10 @@ A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integr
A [website/system](http://www.wiki.com/) that allows for collaborative editing of its content by the users. In programming, wikis usually contain documentation of how to use the software.
+### Working area
+
+Files that have been modified but are not committed. Check them by using the command "git status".
+
### Working Tree
[Consists of files](http://stackoverflow.com/questions/3689838/difference-between-head-working-tree-index-in-git) that you are currently working on.
diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md
new file mode 100644
index 00000000000..9e0d8f79522
--- /dev/null
+++ b/doc/update/10.1-to-10.2.md
@@ -0,0 +1,360 @@
+---
+comments: false
+---
+
+# From 10.1 to 10.2
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:config/gitlab.yml.example origin/10-2-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab-ssl origin/10-2-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab origin/10-2-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:lib/support/init.d/gitlab.default.example origin/10-2-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (10.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index c03700a3501..b9532bf897f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
+| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
@@ -261,5 +262,6 @@ only.
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
+[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index b8dd96087f1..43713855e26 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -21,7 +21,7 @@ want to add.
---
-Select the user and the [permission level](../../user/permissions.md)
+Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 318e054e978..c623a516c47 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should see additional file lines' do
- page.within @diff.parent do
+ page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..."
end
end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index b9c7d443f6c..c1c0d344917 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -42,6 +42,8 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
+ include Gitlab::Utils::StrongMemoize
+
def find_current_user!
user = find_user_from_access_token || find_user_from_warden
return unless user
@@ -52,9 +54,9 @@ module API
end
def access_token
- return @access_token if defined?(@access_token)
-
- @access_token = find_oauth_access_token || find_personal_access_token
+ strong_memoize(:access_token) do
+ find_oauth_access_token || find_personal_access_token
+ end
end
def validate_access_token!(scopes: [])
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index a382db92e8d..16ae99b5c6c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1042,6 +1042,11 @@ module API
expose :value
end
+ class PagesDomainCertificateExpiration < Grape::Entity
+ expose :expired?, as: :expired
+ expose :expiration
+ end
+
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
@@ -1049,12 +1054,23 @@ module API
expose :certificate_text
end
+ class PagesDomainBasic < Grape::Entity
+ expose :domain
+ expose :url
+ expose :certificate,
+ as: :certificate_expiration,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificateExpiration do |pages_domain|
+ pages_domain
+ end
+ end
+
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :certificate,
- if: ->(pages_domain, _) { pages_domain.certificate? },
- using: PagesDomainCertificate do |pages_domain|
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 340a7cecf09..bcf2e6dae1d 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -25,24 +25,7 @@ module API
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
- def present_groups(groups, options = {})
- options = options.reverse_merge(
- with: Entities::Group,
- current_user: current_user
- )
-
- groups = groups.with_statistics if options[:statistics]
- present paginate(groups), options
- end
- end
-
- resource :groups do
- include CustomAttributesEndpoints
-
- desc 'Get a groups list' do
- success Entities::Group
- end
- params do
+ params :group_list_params do
use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
@@ -52,19 +35,47 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
- get do
+
+ def find_groups(params)
find_params = {
all_available: params[:all_available],
- owned: params[:owned],
- custom_attributes: params[:custom_attributes]
+ custom_attributes: params[:custom_attributes],
+ owned: params[:owned]
}
+ find_params[:parent] = find_group!(params[:id]) if params[:id]
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
- present_groups groups, statistics: params[:statistics] && current_user.admin?
+ groups
+ end
+
+ def present_groups(params, groups)
+ options = {
+ with: Entities::Group,
+ current_user: current_user,
+ statistics: params[:statistics] && current_user.admin?
+ }
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
+ end
+
+ resource :groups do
+ include CustomAttributesEndpoints
+
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get do
+ groups = find_groups(params)
+ present_groups params, groups
end
desc 'Create a group. Available only for users who can create groups.' do
@@ -166,6 +177,17 @@ module API
present paginate(projects), with: entity, current_user: current_user
end
+ desc 'Get a list of subgroups in this group.' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get ":id/subgroups" do
+ groups = find_groups(params)
+ present_groups params, groups
+ end
+
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 5f9b94cc89c..3c8960cb1ab 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -155,6 +155,11 @@ module API
end
end
+ def authenticated_with_full_private_access!
+ authenticate!
+ forbidden! unless current_user.full_private_access?
+ end
+
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.admin?
@@ -190,6 +195,10 @@ module API
not_found! unless user_project.pages_available?
end
+ def require_pages_config_enabled!
+ not_found! unless Gitlab.config.pages.enabled
+ end
+
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 3c1c412ba42..a116ab3c9bd 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -136,7 +136,7 @@ module API
authorize_update_builds!
build = find_build!(params[:job_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 259f3f34068..d7b613a717e 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -4,7 +4,6 @@ module API
before do
authenticate!
- require_pages_enabled!
end
after_validation do
@@ -29,10 +28,31 @@ module API
end
end
+ resource :pages do
+ before do
+ require_pages_config_enabled!
+ authenticated_with_full_private_access!
+ end
+
+ desc "Get all pages domains" do
+ success Entities::PagesDomainBasic
+ end
+ params do
+ use :pagination
+ end
+ get "domains" do
+ present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
+ before do
+ require_pages_enabled!
+ end
+
desc 'Get all pages domains' do
success Entities::PagesDomain
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 6454e475036..bbcc851d07a 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -522,6 +522,12 @@ module API
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username to use to post the message'
}
],
'teamcity' => [
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index f493fd7c7ec..fa0bef39602 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -169,7 +169,7 @@ module API
authorize_update_builds!
build = get_build!(params[:build_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index c88eb9783ed..67a39d28944 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -51,10 +51,20 @@ module Gitlab
FROM projects
WHERE forked_project_links.forked_from_project_id = projects.id
)
+ AND NOT EXISTS (
+ SELECT true
+ FROM forked_project_links AS parent_links
+ WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ AND NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE parent_links.forked_from_project_id = projects.id
+ )
+ )
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
MISSING_MEMBERS
- ForkNetworkMember.count_by_sql(count_sql) > 0
+ ForkedProjectLink.count_by_sql(count_sql) > 0
end
def log(message)
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index 27a95764dc1..f7276a380dc 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -15,7 +15,10 @@ module Gitlab
return false unless new_lfs_pointers.present?
- existing_count = @project.lfs_objects.where(oid: new_lfs_pointers.map(&:lfs_oid)).count
+ existing_count = @project.lfs_storage_project
+ .lfs_objects
+ .where(oid: new_lfs_pointers.map(&:lfs_oid))
+ .count
existing_count != new_lfs_pointers.count
end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index ab94ba8a73a..e36d5410431 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -72,7 +72,7 @@ module Gitlab
# Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
# it would be created from `start_branch_name`.
- # If `start_project` is passed, and the branch doesn't exist,
+ # If `start_repository` is passed, and the branch doesn't exist,
# it would try to find the commits from it instead of current repository.
def with_branch(
branch_name,
@@ -80,15 +80,13 @@ module Gitlab
start_repository: repository,
&block)
- # Refactoring aid
- unless start_repository.is_a?(Gitlab::Git::Repository)
- raise "expected a Gitlab::Git::Repository, got #{start_repository}"
- end
+ Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
start_branch_name = nil if start_repository.empty_repo?
if start_branch_name && !start_repository.branch_exists?(start_branch_name)
- raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
+ raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}"
end
update_branch_with_hooks(branch_name) do
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
new file mode 100644
index 00000000000..3685aa20669
--- /dev/null
+++ b/lib/gitlab/git/remote_repository.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Git
+ #
+ # When a Gitaly call involves two repositories instead of one we cannot
+ # assume that both repositories are on the same Gitaly server. In this
+ # case we need to make a distinction between the repository that the
+ # call is being made on (a Repository instance), and the "other"
+ # repository (a RemoteRepository instance). This is the reason why we
+ # have the RemoteRepository class in Gitlab::Git.
+ #
+ # When you make changes, be aware that gitaly-ruby sub-classes this
+ # class.
+ #
+ class RemoteRepository
+ attr_reader :path, :relative_path, :gitaly_repository
+
+ def initialize(repository)
+ @relative_path = repository.relative_path
+ @gitaly_repository = repository.gitaly_repository
+
+ # These instance variables will not be available in gitaly-ruby, where
+ # we have no disk access to this repository.
+ @repository = repository
+ @path = repository.path
+ end
+
+ def empty_repo?
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.empty_repo?
+ end
+
+ def commit_id(revision)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.commit(revision)&.sha
+ end
+
+ def branch_exists?(name)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.branch_exists?(name)
+ end
+
+ # Compares self to a Gitlab::Git::Repository. This implementation uses
+ # 'self.gitaly_repository' so that it will also work in the
+ # GitalyRemoteRepository subclass defined in gitaly-ruby.
+ def same_repository?(other_repository)
+ gitaly_repository.storage_name == other_repository.storage &&
+ gitaly_repository.relative_path == other_repository.relative_path
+ end
+
+ def fetch_env
+ gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
+ gitaly_address = gitaly_client.address(storage)
+ gitaly_token = gitaly_client.token(storage)
+
+ request = Gitaly::SSHUploadPackRequest.new(repository: gitaly_repository)
+ env = {
+ 'GITALY_ADDRESS' => gitaly_address,
+ 'GITALY_PAYLOAD' => request.to_json,
+ 'GITALY_WD' => Dir.pwd,
+ 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
+ }
+ env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
+
+ env
+ end
+
+ private
+
+ # Must return an object that responds to 'address' and 'storage'.
+ def gitaly_client
+ Gitlab::GitalyClient
+ end
+
+ def storage
+ gitaly_repository.storage_name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index d236e1b03e6..cfb88a0c12b 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -58,7 +58,7 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
- attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver
+ attr_reader :storage, :gl_repository, :relative_path
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
@@ -66,7 +66,6 @@ module Gitlab
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
- @gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@@ -105,7 +104,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
@@ -1014,23 +1013,22 @@ module Gitlab
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
return yield nil if start_repository.empty_repo?
- if start_repository == self
+ if start_repository.same_repository?(self)
yield commit(start_branch_name)
else
- start_commit = start_repository.commit(start_branch_name)
+ start_commit_id = start_repository.commit_id(start_branch_name)
- return yield nil unless start_commit
+ return yield nil unless start_commit_id
- sha = start_commit.sha
-
- if branch_commit = commit(sha)
+ if branch_commit = commit(start_commit_id)
yield branch_commit
else
with_repo_tmp_commit(
- start_repository, start_branch_name, sha) do |tmp_commit|
+ start_repository, start_branch_name, start_commit_id) do |tmp_commit|
yield tmp_commit
end
end
@@ -1087,6 +1085,9 @@ module Gitlab
end
def fetch_ref(source_repository, source_ref:, target_ref:)
+ Gitlab::Git.check_namespace!(source_repository)
+ source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository)
+
message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled|
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
@@ -1620,22 +1621,9 @@ module Gitlab
end
def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
- gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
- gitaly_address = gitaly_resolver.address(source_repository.storage)
- gitaly_token = gitaly_resolver.token(source_repository.storage)
-
- request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository)
- env = {
- 'GITALY_ADDRESS' => gitaly_address,
- 'GITALY_PAYLOAD' => request.to_json,
- 'GITALY_WD' => Dir.pwd,
- 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
- }
- env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
-
args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
- run_git(args, env: env)
+ run_git(args, env: source_repository.fetch_env)
end
def gitaly_ff_merge(user, source_sha, target_branch)
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 5d36f16abd4..022d1f249a9 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -59,7 +59,13 @@ module Gitlab
end
def pages
- gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
+ @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
+ if is_enabled
+ gitaly_get_all_pages
+ else
+ gollum_get_all_pages
+ end
+ end
end
def page(title:, version: nil, dir: nil)
@@ -179,6 +185,10 @@ module Gitlab
Gitlab::Git::WikiFile.new(gollum_file)
end
+ def gollum_get_all_pages
+ gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
+ end
+
def gitaly_write_page(name, format, content, commit_details)
gitaly_wiki_client.write_page(name, format, content, commit_details)
end
@@ -204,6 +214,12 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
+
+ def gitaly_get_all_pages
+ gitaly_wiki_client.get_all_pages.map do |wiki_page, version|
+ Gitlab::Git::WikiPage.new(wiki_page, version)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
index 8226278d5f6..98d96fe6211 100644
--- a/lib/gitlab/gitaly_client/wiki_page.rb
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -11,6 +11,10 @@ module Gitlab
FIELDS.each do |field|
instance_variable_set("@#{field}", params[field])
end
+
+ # All gRPC strings in a response are frozen, so we get an unfrozen
+ # version here so appending to `raw_data` doesn't blow up.
+ @raw_data = @raw_data.dup
end
def historical?
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 1a668338f57..8f05f40365e 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -81,28 +81,23 @@ module Gitlab
)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
- wiki_page = version = nil
- response.each do |message|
- page = message.page
- next unless page
+ wiki_page_from_iterator(response)
+ end
- if wiki_page
- wiki_page.raw_data << page.raw_data
- else
- wiki_page = GitalyClient::WikiPage.new(page.to_h)
- # All gRPC strings in a response are frozen, so we get
- # an unfrozen version here so appending in the else clause below doesn't blow up.
- wiki_page.raw_data = wiki_page.raw_data.dup
+ def get_all_pages
+ request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request)
+ pages = []
- version = Gitlab::Git::WikiPageVersion.new(
- Gitlab::Git::Commit.decorate(@repository, page.version.commit),
- page.version.format
- )
- end
+ loop do
+ page, version = wiki_page_from_iterator(response) { |message| message.end_of_page }
+
+ break unless page && version
+ pages << [page, version]
end
- [wiki_page, version]
+ pages
end
def find_file(name, revision)
@@ -133,6 +128,35 @@ module Gitlab
private
+ # If a block is given and the yielded value is true, iteration will be
+ # stopped early at that point; else the iterator is consumed entirely.
+ # The iterator is traversed with `next` to allow resuming the iteration.
+ def wiki_page_from_iterator(iterator)
+ wiki_page = version = nil
+
+ while message = iterator.next
+ break if block_given? && yield(message)
+
+ page = message.page
+ next unless page
+
+ if wiki_page
+ wiki_page.raw_data << page.raw_data
+ else
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+
+ version = Gitlab::Git::WikiPageVersion.new(
+ Gitlab::Git::Commit.decorate(@repository, page.version.commit),
+ page.version.format
+ )
+ end
+ end
+
+ [wiki_page, version]
+ rescue StopIteration
+ [wiki_page, version]
+ end
+
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
name: GitalyClient.encode(commit_details.name),
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 3a666c2268b..dfcdfc307b6 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -20,7 +20,7 @@ module Gitlab
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
- gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg')
+ gon.sprite_icons = IconsHelper.sprite_icon_path
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
index de9cab80a02..196f2b6b34c 100644
--- a/lib/gitlab/hook_data/issue_builder.rb
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -4,7 +4,6 @@ module Gitlab
SAFE_HOOK_ATTRIBUTES = %i[
assignee_id
author_id
- branch_name
closed_at
confidential
created_at
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index 55c707d5386..df4bdf16847 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,8 +11,6 @@ module Gitlab
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
-
- worker.metrics_tags.each { |tag, value| trans.add_tag(tag, value) } if worker.respond_to?(:metrics_tags)
rescue Exception => error # rubocop: disable Lint/RescueException
trans.add_event(:sidekiq_exception)
diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb
new file mode 100644
index 00000000000..a2ac9285b56
--- /dev/null
+++ b/lib/gitlab/utils/strong_memoize.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Utils
+ module StrongMemoize
+ # Instead of writing patterns like this:
+ #
+ # def trigger_from_token
+ # return @trigger if defined?(@trigger)
+ #
+ # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ #
+ # We could write it like:
+ #
+ # def trigger_from_token
+ # strong_memoize(:trigger) do
+ # Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ # end
+ #
+ def strong_memoize(name)
+ ivar_name = "@#{name}"
+
+ if instance_variable_defined?(ivar_name)
+ instance_variable_get(ivar_name)
+ else
+ instance_variable_set(ivar_name, yield)
+ end
+ end
+ end
+ end
+end
diff --git a/package.json b/package.json
index 6315f9eced9..e607981143d 100644
--- a/package.json
+++ b/package.json
@@ -61,8 +61,6 @@
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
"timeago.js": "^2.0.5",
- "ts-loader": "^3.1.1",
- "typescript": "^2.6.1",
"underscore": "^1.8.3",
"url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
diff --git a/qa/.gitignore b/qa/.gitignore
index 3fec32c8427..19ec17d0005 100644
--- a/qa/.gitignore
+++ b/qa/.gitignore
@@ -1 +1,2 @@
tmp/
+.ruby-version
diff --git a/qa/bin/qa b/qa/bin/qa
index cecdeac14db..6a772e93cee 100755
--- a/qa/bin/qa
+++ b/qa/bin/qa
@@ -4,4 +4,4 @@ require_relative '../qa'
QA::Scenario
.const_get(ARGV.shift)
- .perform(*ARGV)
+ .launch!(ARGV)
diff --git a/qa/qa.rb b/qa/qa.rb
index e8689a44f4d..dc1cd9abc6a 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -18,6 +18,7 @@ module QA
##
# Support files
#
+ autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
autoload :Entrypoint, 'qa/scenario/entrypoint'
autoload :Template, 'qa/scenario/template'
@@ -61,6 +62,7 @@ module QA
module Main
autoload :Entry, 'qa/page/main/entry'
+ autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b9e199000d6..59cd147e055 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -23,7 +23,7 @@ module QA
def password=(pass)
@password = pass
- @uri.password = pass
+ @uri.password = CGI.escape(pass)
end
def use_default_credentials
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index d55326c5262..bdddfb877c5 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -5,7 +5,7 @@ module QA
include Scenario::Actable
def refresh
- visit current_path
+ visit current_url
end
end
end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index a9810beeb29..ac939732b1d 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -2,9 +2,14 @@ module QA
module Page
module Main
class Entry < Page::Base
- def initialize
- visit('/')
+ def visit_login_page
+ visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
+ wait_for_instance_to_be_ready
+ end
+
+ private
+ def wait_for_instance_to_be_ready
# This resolves cold boot / background tasks problems
#
start = Time.now
@@ -14,18 +19,6 @@ module QA
refresh
end
end
-
- def sign_in_using_credentials
- if page.has_content?('Change your password')
- fill_in :user_password, with: Runtime::User.password
- fill_in :user_password_confirmation, with: Runtime::User.password
- click_button 'Change your password'
- end
-
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
- click_button 'Sign in'
- end
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
new file mode 100644
index 00000000000..8b0111a78a2
--- /dev/null
+++ b/qa/qa/page/main/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Main
+ class Login < Page::Base
+ def sign_in_using_credentials
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
index 2001dc5b230..42ab9c6f675 100644
--- a/qa/qa/page/mattermost/login.rb
+++ b/qa/qa/page/mattermost/login.rb
@@ -3,7 +3,7 @@ module QA
module Mattermost
class Login < Page::Base
def initialize
- visit(Runtime::Scenario.mattermost + '/login')
+ visit(Runtime::Scenario.mattermost_address + '/login')
end
def sign_in_using_oauth
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
index e636d7676f4..4b8fc28e53f 100644
--- a/qa/qa/page/mattermost/main.rb
+++ b/qa/qa/page/mattermost/main.rb
@@ -3,7 +3,7 @@ module QA
module Mattermost
class Main < Page::Base
def initialize
- visit(Runtime::Scenario.mattermost)
+ visit(Runtime::Scenario.mattermost_address)
end
end
end
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
index 0c5e9787e17..7ef59046640 100644
--- a/qa/qa/runtime/scenario.rb
+++ b/qa/qa/runtime/scenario.rb
@@ -1,8 +1,28 @@
module QA
module Runtime
+ ##
+ # Singleton approach to global test scenario arguments.
+ #
module Scenario
extend self
- attr_accessor :mattermost
+
+ attr_reader :attributes
+
+ def define(attribute, value)
+ (@attributes ||= {}).store(attribute.to_sym, value)
+
+ define_singleton_method(attribute) do
+ @attributes[attribute.to_sym].tap do |value|
+ if value.to_s.empty?
+ raise ArgumentError, "Empty `#{attribute}` attribute!"
+ end
+ end
+ end
+ end
+
+ def method_missing(name, *)
+ raise ArgumentError, "Scenario attribute `#{name}` not defined!"
+ end
end
end
end
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
new file mode 100644
index 00000000000..cf8996cd597
--- /dev/null
+++ b/qa/qa/scenario/bootable.rb
@@ -0,0 +1,45 @@
+require 'optparse'
+
+module QA
+ module Scenario
+ module Bootable
+ Option = Struct.new(:name, :arg, :desc)
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def launch!(argv)
+ return self.perform(*argv) unless has_attributes?
+
+ arguments = OptionParser.new do |parser|
+ options.to_a.each do |opt|
+ parser.on(opt.arg, opt.desc) do |value|
+ Runtime::Scenario.define(opt.name, value)
+ end
+ end
+ end
+
+ arguments.parse!(argv)
+
+ self.perform(**Runtime::Scenario.attributes)
+ end
+
+ private
+
+ def attribute(name, arg, desc)
+ options.push(Option.new(name, arg, desc))
+ end
+
+ def options
+ @options ||= []
+ end
+
+ def has_attributes?
+ options.any?
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb
index 33cb2696f8f..ae099fd911e 100644
--- a/qa/qa/scenario/entrypoint.rb
+++ b/qa/qa/scenario/entrypoint.rb
@@ -5,18 +5,10 @@ module QA
# including staging and on-premises installation.
#
class Entrypoint < Template
- def self.tags(*tags)
- @tags = tags
- end
-
- def self.get_tags
- @tags
- end
+ include Bootable
def perform(address, *files)
- Specs::Config.perform do |specs|
- specs.address = address
- end
+ Runtime::Scenario.define(:gitlab_address, address)
##
# Perform before hooks, which are different for CE and EE
@@ -24,13 +16,19 @@ module QA
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs|
- specs.rspec(
- tty: true,
- tags: self.class.get_tags,
- files: files.any? ? files : 'qa/specs/features'
- )
+ specs.tty = true
+ specs.tags = self.class.get_tags
+ specs.files = files.any? ? files : 'qa/specs/features'
end
end
+
+ def self.tags(*tags)
+ @tags = tags
+ end
+
+ def self.get_tags
+ @tags
+ end
end
end
end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index 0220da929ce..7d0702afdb1 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -10,7 +10,8 @@ module QA
tags :core, :mattermost
def perform(address, mattermost, *files)
- Runtime::Scenario.mattermost = mattermost
+ Runtime::Scenario.define(:mattermost_address, mattermost)
+
super(address, *files)
end
end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 79c681168cc..591ddde8ab9 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -9,15 +9,7 @@ require 'selenium-webdriver'
module QA
module Specs
class Config < Scenario::Template
- attr_writer :address
-
- def initialize
- @address = ENV['GITLAB_URL']
- end
-
def perform
- raise 'Please configure GitLab address!' unless @address
-
configure_rspec!
configure_capybara!
end
@@ -56,7 +48,6 @@ module QA
end
Capybara.configure do |config|
- config.app_host = @address
config.default_driver = :chrome
config.javascript_driver = :chrome
config.default_max_wait_time = 4
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index ba19ce17ee5..b155708c387 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'standard root login', :core do
scenario 'user logs in using credentials' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index c4afd83c8e4..853a9a6a4f4 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'create a new group', :mattermost do
scenario 'creating a group with a mattermost team' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
index a89a6a3d1cf..92f91cb2725 100644
--- a/qa/qa/specs/features/mattermost/login_spec.rb
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'logging in to Mattermost', :mattermost do
scenario 'can use gitlab oauth' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Page::Mattermost::Login.act { sign_in_using_oauth }
Page::Mattermost::Main.perform do |page|
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index 27eb22f15a6..aba0c2b4c14 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,7 +1,8 @@
module QA
feature 'create a new project', :core do
scenario 'user creates a new project' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project|
project.name = 'awesome-project'
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 3571173783d..5cc3b3b9c1b 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -9,7 +9,8 @@ module QA
end
before do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project-with-code'
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 0e691fb0d75..30935dc1e13 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -2,7 +2,8 @@ module QA
feature 'push code to repository', :core do
context 'with regular account over http' do
scenario 'user pushes code to the repository' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project_with_code'
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 2aa18d5d3a1..f98b8f88e9a 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -2,16 +2,22 @@ require 'rspec/core'
module QA
module Specs
- class Runner
- include Scenario::Actable
+ class Runner < Scenario::Template
+ attr_accessor :tty, :tags, :files
- def rspec(tty: false, tags: [], files: ['qa/specs/features'])
+ def initialize
+ @tty = false
+ @tags = []
+ @files = ['qa/specs/features']
+ end
+
+ def perform
args = []
- args << '--tty' if tty
- tags.to_a.each do |tag|
- args << ['-t', tag.to_s]
- end
- args << files
+ args.push('--tty') if tty
+ tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
+ args.push(files)
+
+ Specs::Config.perform
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
diff --git a/qa/spec/runtime/scenario_spec.rb b/qa/spec/runtime/scenario_spec.rb
new file mode 100644
index 00000000000..7009192bcc0
--- /dev/null
+++ b/qa/spec/runtime/scenario_spec.rb
@@ -0,0 +1,27 @@
+describe QA::Runtime::Scenario do
+ subject do
+ Module.new.extend(described_class)
+ end
+
+ it 'makes it possible to define global scenario attributes' do
+ subject.define(:my_attribute, 'some-value')
+ subject.define(:another_attribute, 'another-value')
+
+ expect(subject.my_attribute).to eq 'some-value'
+ expect(subject.another_attribute).to eq 'another-value'
+ expect(subject.attributes)
+ .to eq(my_attribute: 'some-value', another_attribute: 'another-value')
+ end
+
+ it 'raises error when attribute is not known' do
+ expect { subject.invalid_accessor }
+ .to raise_error ArgumentError, /invalid_accessor/
+ end
+
+ it 'raises error when attribute is empty' do
+ subject.define(:empty_attribute, '')
+
+ expect { subject.empty_attribute }
+ .to raise_error ArgumentError, /empty_attribute/
+ end
+end
diff --git a/qa/spec/scenario/bootable_spec.rb b/qa/spec/scenario/bootable_spec.rb
new file mode 100644
index 00000000000..273aac7677e
--- /dev/null
+++ b/qa/spec/scenario/bootable_spec.rb
@@ -0,0 +1,24 @@
+describe QA::Scenario::Bootable do
+ subject do
+ Class.new(QA::Scenario::Template)
+ .include(described_class)
+ end
+
+ it 'makes it possible to define the scenario attribute' do
+ subject.class_eval do
+ attribute :something, '--something SOMETHING', 'Some attribute'
+ attribute :another, '--another ANOTHER', 'Some other attribute'
+ end
+
+ expect(subject).to receive(:perform)
+ .with(something: 'test', another: 'other')
+
+ subject.launch!(%w[--another other --something test])
+ end
+
+ it 'does not require attributes to be defined' do
+ expect(subject).to receive(:perform).with('some', 'argv')
+
+ subject.launch!(%w[some argv])
+ end
+end
diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/entrypoint_spec.rb
index 3fd068b641c..aec79dcea04 100644
--- a/qa/spec/scenario/entrypoint_spec.rb
+++ b/qa/spec/scenario/entrypoint_spec.rb
@@ -6,31 +6,30 @@ describe QA::Scenario::Entrypoint do
end
context '#perform' do
- let(:config) { spy('Specs::Config') }
+ let(:arguments) { spy('Runtime::Scenario') }
let(:release) { spy('Runtime::Release') }
let(:runner) { spy('Specs::Runner') }
before do
- allow(config).to receive(:perform) { |&block| block.call config }
- allow(runner).to receive(:perform) { |&block| block.call runner }
-
- stub_const('QA::Specs::Config', config)
stub_const('QA::Runtime::Release', release)
+ stub_const('QA::Runtime::Scenario', arguments)
stub_const('QA::Specs::Runner', runner)
+
+ allow(runner).to receive(:perform).and_yield(runner)
end
- it 'should set address' do
+ it 'sets an address of the subject' do
subject.perform("hello")
- expect(config).to have_received(:address=).with("hello")
+ expect(arguments).to have_received(:define)
+ .with(:gitlab_address, "hello")
end
context 'no paths' do
it 'should call runner with default arguments' do
subject.perform("test")
- expect(runner).to have_received(:rspec)
- .with(hash_including(files: 'qa/specs/features'))
+ expect(runner).to have_received(:files=).with('qa/specs/features')
end
end
@@ -38,8 +37,7 @@ describe QA::Scenario::Entrypoint do
it 'should call runner with paths' do
subject.perform('test', 'path1', 'path2')
- expect(runner).to have_received(:rspec)
- .with(hash_including(files: %w(path1 path2)))
+ expect(runner).to have_received(:files=).with(%w[path1 path2])
end
end
end
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
new file mode 100644
index 00000000000..28f6cfb50ae
--- /dev/null
+++ b/scripts/create_mysql_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+mysql --user=root --host=mysql <<EOF
+CREATE DATABASE IF NOT EXISTS gitlabhq_test;
+CREATE USER IF NOT EXISTS 'gitlab'@'%';
+GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
+FLUSH PRIVILEGES;
+EOF
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
new file mode 100644
index 00000000000..8a744df3226
--- /dev/null
+++ b/scripts/create_postgres_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+psql -h postgres -U postgres postgres <<EOF
+DROP DATABASE IF EXISTS gitlabhq_test;
+CREATE DATABASE gitlabhq_test;
+CREATE USER gitlab;
+GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+EOF
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 7abadef5e89..36bcf087cd9 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,6 +1,7 @@
. scripts/utils.sh
export SETUP_DB=${SETUP_DB:-true}
+export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
@@ -26,6 +27,9 @@ fi
cp config/database.yml.$GITLAB_DATABASE config/database.yml
+# Set user to a non-superuser to ensure we test permissions
+sed -i 's/username: root/username: gitlab/g' config/database.yml
+
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
sed -i 's/localhost/postgres/g' config/database.yml
else # Assume it's mysql
@@ -44,6 +48,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
+# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
+# user but not necessarily a full schema loaded
+if [ "$CREATE_DB_USER" != "false" ]; then
+ if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+fi
+
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index cd3bf785d34..d7825364ed5 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -12,12 +12,14 @@ describe IssuableCollections do
controller = klass.new
- allow(controller).to receive(:params).and_return(state: 'opened')
+ allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
controller
end
describe '#page_count_for_relation' do
+ let(:params) { { state: 'opened' } }
+
it 'returns the number of pages' do
relation = double(:relation, limit_value: 20)
pages = controller.send(:page_count_for_relation, relation, 28)
@@ -25,4 +27,55 @@ describe IssuableCollections do
expect(pages).to eq(2)
end
end
+
+ describe '#filter_params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'true',
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
+ end
+
+ it 'filters params' do
+ allow(controller).to receive(:cookies).and_return({})
+
+ filtered_params = controller.send(:filter_params)
+
+ expect(filtered_params).to eq({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'authorized_only' => 'true',
+ 'due_date' => '2017-01-01',
+ 'group_id' => '3',
+ 'iids' => '4',
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'non_archived' => 'true',
+ 'project_id' => '5',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
+ end
+ end
end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index d862e1447e3..f9faa4fa59a 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -44,11 +44,11 @@ describe Dashboard::TodosController do
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
- let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
+ let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
it 'redirects to last_page if page number is larger than number of pages' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index f9688949a19..7490f8fefce 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -371,8 +371,10 @@ describe Projects::JobsController do
end
describe 'POST erase' do
+ let(:role) { :master }
+
before do
- project.add_developer(user)
+ project.team << [user, role]
sign_in(user)
post_erase
@@ -404,6 +406,27 @@ describe Projects::JobsController do
end
end
+ context 'when user is developer' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) }
+
+ context 'when triggered by same user' do
+ let(:triggered_by) { user }
+
+ it 'has successful status' do
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ context 'when triggered by different user' do
+ let(:triggered_by) { create(:user) }
+
+ it 'does not have successful status' do
+ expect(response).not_to have_gitlab_http_status(:found)
+ end
+ end
+ end
+
def post_erase
post :erase, namespace_id: project.namespace,
project_id: project,
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index 9039b283393..da1e17225db 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -44,7 +44,11 @@ feature 'Groups > Members > Manage members' do
visit group_group_members_path(group)
- find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ accept_confirm do
+ find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
expect(page).not_to have_content(user2.name)
expect(group.users).not_to include(user2)
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index b8a66245153..95d637265e0 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
+ end
- def expect_to_wrap(should_wrap, item, note, value)
- expect(item).to have_content(value)
- expect(item).not_to have_content("\"#{value}\"")
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
- item.click
+ item.click
- if should_wrap
- expect(note.value).to include("\"#{value}\"")
- else
- expect(note.value).not_to include("\"#{value}\"")
- end
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
end
end
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 6fbee0ebcb5..4224a8fe5d4 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -1,9 +1,9 @@
require 'rails_helper'
feature 'Issue Detail', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, author: user) }
context 'when user displays the issue' do
before do
@@ -27,6 +27,7 @@ feature 'Issue Detail', :js do
click_link 'Edit'
fill_in 'issuable-title', with: 'issue title'
click_button 'Save'
+ wait_for_requests
Users::DestroyService.new(user).execute(user)
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 5402d61da54..db5ce2d11a8 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
+ it 'allows filtering multiple dropdowns' do
+ visit project_new_merge_request_path(project)
+
+ first('.js-source-branch').click
+
+ input = find('.dropdown-source-branch .dropdown-input-field')
+ input.click
+ input.send_keys('orphaned-branch')
+
+ find('.dropdown-source-branch .dropdown-content li', match: :first)
+ source_items = all('.dropdown-source-branch .dropdown-content li')
+
+ expect(source_items.count).to eq(1)
+
+ first('.js-target-branch').click
+
+ find('.dropdown-target-branch .dropdown-content li', match: :first)
+ target_items = all('.dropdown-target-branch .dropdown-content li')
+
+ expect(target_items.count).to be > 1
+ end
+
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 9912e8165e6..7adae08e499 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -79,22 +79,6 @@ feature 'Merge Request filtering by Labels', :js do
end
end
- context 'clear button' do
- before do
- input_filtered_search('label:~bug')
- end
-
- it 'allows user to remove filtered labels' do
- first('.clear-search').click
- filtered_search.send_keys(:enter)
-
- expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
- expect(page).to have_content "Bugfix2"
- expect(page).to have_content "Feature1"
- expect(page).to have_content "Bugfix1"
- end
- end
-
context 'filter dropdown' do
it 'filters by label name' do
init_label_search
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6c9dc67ad74..27efc32c95b 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
end
+
+ feature 'Open a milestone' do
+ scenario 'shows total issue time spent correctly when no time has been logged' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content 'No time spent'
+ expect(page).to have_content 'None'
+ end
+ end
+
+ scenario 'shows total issue time spent' do
+ milestone = create(:milestone, project: project, title: 8.7)
+ issue1 = create(:issue, project: project, milestone: milestone)
+ issue2 = create(:issue, project: project, milestone: milestone)
+ issue1.spend_time(duration: 3600, user: user)
+ issue1.save!
+ issue2.spend_time(duration: 7200, user: user)
+ issue2.save!
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content '3h'
+ end
+ end
+ end
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 237c059e595..65b11a1d9e7 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -55,6 +55,22 @@ feature 'Project members list' do
end
end
+ scenario 'remove user from project', :js do
+ other_user = create(:user)
+ project.add_developer(other_user)
+
+ visit_members_page
+
+ accept_confirm do
+ find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content(other_user.name)
+ expect(project.users).not_to include(other_user)
+ end
+
scenario 'invite user to project', :js do
visit_members_page
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
index 684af74d750..dcf9111776e 100644
--- a/spec/finders/autocomplete_users_finder_spec.rb
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do
it { is_expected.to match_array([user1]) }
end
+ context 'when passed a subgroup', :nested_groups do
+ let(:grandparent) { create(:group, :public) }
+ let(:parent) { create(:group, :public, parent: grandparent) }
+ let(:child) { create(:group, :public, parent: parent) }
+ let(:group) { parent }
+
+ let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user }
+ let!(:parent_user) { create(:group_member, :developer, group: parent).user }
+ let!(:child_user) { create(:group_member, :developer, group: child).user }
+
+ it 'includes users from parent groups as well' do
+ expect(subject).to match_array([grandparent_user, parent_user])
+ end
+ end
+
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
context 'when filtered by search' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
new file mode 100644
index 00000000000..4ba6422406c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate_expiration": {
+ "type": "object",
+ "properties": {
+ "expired": { "type": "boolean" },
+ "expiration": { "type": "string" }
+ },
+ "required": ["expired", "expiration"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
new file mode 100644
index 00000000000..08db8d47050
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate": {
+ "type": "object",
+ "properties": {
+ "subject": { "type": "string" },
+ "expired": { "type": "boolean" },
+ "certificate": { "type": "string" },
+ "certificate_text": { "type": "string" }
+ },
+ "required": ["subject", "expired"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
new file mode 100644
index 00000000000..c7d86de7d8e
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pages_domain/basic.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
index 0de1d0f1228..7c27218dc5a 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
@@ -1,23 +1,4 @@
{
"type": "array",
- "items": {
- "type": "object",
- "properties": {
- "domain": { "type": "string" },
- "url": { "type": "uri" },
- "certificate": {
- "type": "object",
- "properties": {
- "subject": { "type": "string" },
- "expired": { "type": "boolean" },
- "certificate": { "type": "string" },
- "certificate_text": { "type": "string" }
- },
- "required": ["subject", "expired"],
- "additionalProperties": false
- }
- },
- "required": ["domain", "url"],
- "additionalProperties": false
- }
+ "items": { "$ref": "pages_domain/detail.json" }
}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 7a241b02d28..5c5d53877a6 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
describe ApplicationHelper do
include UploadHelpers
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
-
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -57,30 +55,11 @@ describe ApplicationHelper do
end
describe 'project_icon' do
- let(:asset_host) { 'http://assets' }
-
it 'returns an url for the avatar' do
project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
-
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- avatar_url = "#{asset_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- end
-
- it 'gives uploaded icon when present' do
- project = create(:project)
- allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
-
- avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
@@ -91,40 +70,7 @@ describe ApplicationHelper do
context 'when there is a matching user' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user.email).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
-
- context 'when an asset_host is set in the config' do
- let(:asset_host) { 'http://assets' }
-
- before do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- end
-
- it 'returns an absolute URL on that asset host' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{asset_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is set to false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when the GitLab instance is at a relative URL' do
- before do
- stub_config_setting(relative_url_root: '/gitlab')
- # Must be stubbed after the stub above, and separately
- stub_config_setting(url: Settings.send(:build_gitlab_url))
- end
-
- it 'returns a relative URL with the correct prefix' do
- expect(helper.avatar_icon(user.email).to_s)
- .to eq("/gitlab/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ .to eq(user.avatar.url)
end
end
@@ -138,18 +84,9 @@ describe ApplicationHelper do
end
describe 'using a user' do
- context 'when only_path is true' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: true).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon(user).to_s)
+ .to eq(user.avatar.url)
end
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 97f0ed4904e..32432ee1e81 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
describe GroupsHelper do
include ApplicationHelper
- let(:asset_host) { 'http://assets' }
-
describe 'group_icon' do
avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
@@ -13,16 +11,8 @@ describe GroupsHelper do
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
- avatar_url = "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
-
- expect(helper.group_icon(group).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
-
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- avatar_url = "#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
-
expect(helper.group_icon(group).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ .to eq "<img data-src=\"#{group.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
@@ -34,25 +24,7 @@ describe GroupsHelper do
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
expect(group_icon_url(group.path).to_s)
- .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
- end
-
- it 'returns an CDN url for the avatar' do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- group = create(:group)
- group.avatar = fixture_file_upload(avatar_file_path)
- group.save!
- expect(group_icon_url(group.path).to_s)
- .to match("#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
- end
-
- it 'returns an based url for the avatar if private' do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- group = create(:group, :private)
- group.avatar = fixture_file_upload(avatar_file_path)
- group.save!
- expect(group_icon_url(group.path).to_s)
- .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
+ .to match(group.avatar.url)
end
it 'gives default avatar_icon when no avatar is present' do
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 3d79dac284f..2f23ed55d99 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe IconsHelper do
+ let(:icons_path) { ActionController::Base.helpers.image_path("icons.svg") }
+
describe 'icon' do
it 'returns aria-hidden by default' do
star = icon('star')
@@ -16,22 +18,42 @@ describe IconsHelper do
end
end
+ describe 'sprite_icon_path' do
+ it 'returns relative path' do
+ expect(sprite_icon_path)
+ .to eq icons_path
+ end
+
+ context 'when an asset_host is set in the config it will return an absolute local URL' do
+ let(:asset_host) { 'http://assets' }
+
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ end
+
+ it 'returns an absolute URL on that asset host' do
+ expect(sprite_icon_path)
+ .to eq ActionController::Base.helpers.image_path("icons.svg", host: Gitlab.config.gitlab.url)
+ end
+ end
+ end
+
describe 'sprite_icon' do
icon_name = 'clock'
it 'returns svg icon html' do
expect(sprite_icon(icon_name).to_s)
- .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
- .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
- .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
end
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index f209328dee1..230c15e5de6 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -396,6 +396,25 @@ describe('Filtered Search Manager', () => {
});
});
+ describe('Clearing search', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('Clicking the "x" clear button, clears the input', () => {
+ const inputValue = 'label:~bug ';
+ manager.filteredSearchInput.value = inputValue;
+ manager.filteredSearchInput.dispatchEvent(new Event('input'));
+
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue);
+
+ manager.clearSearchButton.click();
+
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
describe('toggleInputContainerFocus', () => {
beforeEach(() => {
initializeManager();
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index ad0c7264616..6f357306ec7 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () {
});
});
+ describe('DefaultOptions.beforeInsert', () => {
+ const beforeInsert = (context, value) => (
+ gfmAutoCompleteCallbacks.beforeInsert.call(context, value)
+ );
+
+ const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
+
+ it('should not quote if value only contains alphanumeric charecters', () => {
+ expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
+ expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1');
+ });
+
+ it('should quote if value contains any non-alphanumeric characters', () => {
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
+ expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
+ });
+
+ it('should quote integer labels', () => {
+ expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
+ });
+ });
+
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 83c92deccdc..fcf27f6805f 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,4 @@
-import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -52,4 +52,29 @@ describe('Number Utils', () => {
expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
});
});
+
+ describe('bytesToGiB', () => {
+ it('calculates GiB for the given bytes', () => {
+ expect(bytesToGiB(1073741824)).toEqual(1);
+ expect(bytesToGiB(10737418240)).toEqual(10);
+ });
+ });
+
+ describe('numberToHumanSize', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSize(654)).toEqual('654 bytes');
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSize(1079)).toEqual('1.05 KiB');
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSize(10485764)).toEqual('10.00 MiB');
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
new file mode 100644
index 00000000000..a95a7e2a5be
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -0,0 +1,62 @@
+import textUtils from '~/lib/utils/text_markdown';
+
+describe('init markdown', () => {
+ let textArea;
+
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ textArea.focus();
+ });
+
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
+
+ describe('without selection', () => {
+ it('inserts the tag on an empty line', () => {
+ const initialValue = '';
+
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on a new line if the current one is not empty', () => {
+ const initialValue = 'some text';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}\n* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains spaces', () => {
+ const initialValue = ' ';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains tabs', () => {
+ const initialValue = '\t\t\t';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 829b3ef5735..b21bd958f90 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,109 +1,57 @@
-import { highCountTrim } from '~/lib/utils/text_utility';
+import * as textUtils from '~/lib/utils/text_utility';
describe('text_utility', () => {
- describe('gl.text.getTextWidth', () => {
- it('returns zero width when no text is passed', () => {
- expect(gl.text.getTextWidth('')).toBe(0);
+ describe('addDelimiter', () => {
+ it('should add a delimiter to the given string', () => {
+ expect(textUtils.addDelimiter('1234')).toEqual('1,234');
+ expect(textUtils.addDelimiter('222222')).toEqual('222,222');
});
- it('returns zero width when no text is passed and font is passed', () => {
- expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
- });
-
- it('returns width when text is passed', () => {
- expect(gl.text.getTextWidth('foo') > 0).toBe(true);
- });
-
- it('returns bigger width when font is larger', () => {
- const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
- const regular = gl.text.getTextWidth('foo', '10px sans-serif');
- expect(largeFont > regular).toBe(true);
- });
- });
-
- describe('gl.text.pluralize', () => {
- it('returns pluralized', () => {
- expect(gl.text.pluralize('test', 2)).toBe('tests');
- });
-
- it('returns pluralized when count is 0', () => {
- expect(gl.text.pluralize('test', 0)).toBe('tests');
- });
-
- it('does not return pluralized', () => {
- expect(gl.text.pluralize('test', 1)).toBe('test');
+ it('should not add a delimiter if string contains no numbers', () => {
+ expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa');
});
});
describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => {
- expect(highCountTrim(105)).toBe('99+');
- expect(highCountTrim(100)).toBe('99+');
+ expect(textUtils.highCountTrim(105)).toBe('99+');
+ expect(textUtils.highCountTrim(100)).toBe('99+');
});
it('returns exact number for count < 100', () => {
- expect(highCountTrim(45)).toBe(45);
+ expect(textUtils.highCountTrim(45)).toBe(45);
});
});
- describe('gl.text.insertText', () => {
- let textArea;
-
- beforeAll(() => {
- textArea = document.createElement('textarea');
- document.querySelector('body').appendChild(textArea);
- textArea.focus();
+ describe('humanize', () => {
+ it('should remove underscores and uppercase the first letter', () => {
+ expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
});
+ });
- afterAll(() => {
- textArea.parentNode.removeChild(textArea);
+ describe('pluralize', () => {
+ it('should pluralize given string', () => {
+ expect(textUtils.pluralize('test', 2)).toBe('tests');
});
- describe('without selection', () => {
- it('inserts the tag on an empty line', () => {
- const initialValue = '';
-
- textArea.value = initialValue;
- textArea.selectionStart = 0;
- textArea.selectionEnd = 0;
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on a new line if the current one is not empty', () => {
- const initialValue = 'some text';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}\n* `);
- });
-
- it('inserts the tag on the same line if the current line only contains spaces', () => {
- const initialValue = ' ';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on the same line if the current line only contains tabs', () => {
- const initialValue = '\t\t\t';
+ it('should pluralize when count is 0', () => {
+ expect(textUtils.pluralize('test', 0)).toBe('tests');
+ });
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
+ it('should not pluralize when count is 1', () => {
+ expect(textUtils.pluralize('test', 1)).toBe('test');
+ });
+ });
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+ describe('dasherize', () => {
+ it('should replace underscores with dashes', () => {
+ expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo');
+ });
+ });
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ describe('slugify', () => {
+ it('should remove accents and convert to lower case', () => {
+ expect(textUtils.slugify('João')).toEqual('joão');
});
});
});
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index a26fc8f63cc..db75262b562 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -55,6 +55,25 @@ describe('issue_comment_form component', () => {
expect(vm.toggleIssueState).toHaveBeenCalled();
});
+
+ it('should disable action button whilst submitting', (done) => {
+ const saveNotePromise = Promise.resolve();
+ vm.note = 'hello world';
+ spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
+ spyOn(vm, 'stopPolling');
+
+ const actionButton = vm.$el.querySelector('.js-action-button');
+
+ vm.handleSave();
+
+ Vue.nextTick()
+ .then(() => expect(actionButton.disabled).toBeTruthy())
+ .then(saveNotePromise)
+ .then(Vue.nextTick)
+ .then(() => expect(actionButton.disabled).toBeFalsy())
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('textarea', () => {
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
index 376c291c64b..820a44992b4 100644
--- a/spec/javascripts/repo/helpers.js
+++ b/spec/javascripts/repo/helpers.js
@@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({
url: 'url',
name,
path: name,
- last_commit: {
- id: '123',
- message: 'test',
- committed_date: new Date().toISOString(),
- },
});
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
new file mode 100644
index 00000000000..af9d6835a67
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/branch_spec.js
@@ -0,0 +1,38 @@
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore } from '../../helpers';
+
+describe('Multi-file store branch actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('createNewBranch', () => {
+ beforeEach(() => {
+ spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
+ json: () => ({
+ name: 'testing',
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ store.state.project.id = 2;
+ store.state.currentBranch = 'testing';
+ });
+
+ it('creates new branch', (done) => {
+ store.dispatch('createNewBranch', 'master')
+ .then(() => {
+ expect(store.state.currentBranch).toBe('testing');
+ expect(service.createBranch).toHaveBeenCalledWith(2, {
+ branch: 'master',
+ ref: 'testing',
+ });
+ expect(history.pushState).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
new file mode 100644
index 00000000000..099c0556e71
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/file_spec.js
@@ -0,0 +1,417 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+ let getLastCommitDataSpy;
+ let oldGetLastCommitData;
+
+ beforeEach(() => {
+ getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+
+ localFile = file();
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+
+ spyOn(history, 'pushState');
+ });
+
+ afterEach(() => {
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+ });
+
+ it('closes open files', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if has changed', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if temp file', (done) => {
+ localFile.tempFile = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('force closes a changed file', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile, force: true })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState when no open files are left', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets next file as active', (done) => {
+ const f = file();
+ store.state.openFiles.push(f);
+
+ expect(f.active).toBeFalsy();
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', (done) => {
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file active', (done) => {
+ const localFile = file();
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('returns early if file is already active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets current active file to not active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', (done) => {
+ location.hash = 'test';
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile = file();
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () => Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }));
+
+ localFile = file();
+ localFile.url = 'getFileDataURL';
+ });
+
+ it('calls the service', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file data', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets document title', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file as active', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds the file to open files', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles the file loading', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(localFile.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file();
+ });
+
+ it('calls getRawFileData service method', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates file raw data', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file();
+ });
+
+ it('updates file content', (done) => {
+ store.dispatch('changeFileContent', {
+ file: tmpFile,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('createTempFile', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.tree.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds tmp file to open files', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets tmp file as active', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('enters edit mode if file is not base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(store.state.editMode).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not enter edit mode if file is base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ base64: true,
+ }).then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates flash message is file already exists', (done) => {
+ store.state.tree.push(file('test', '1', 'blob'));
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('increases level of file', (done) => {
+ store.state.level = 1;
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.level).toBe(2);
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..393a797c6a3
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -0,0 +1,469 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getTreeData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ Object.assign(store.state.endpoints, {
+ rootEndpoint: 'rootEndpoint',
+ });
+ });
+
+ it('calls service getTreeData', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds data into tree', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree.length).toBe(3);
+ expect(store.state.tree[0].type).toBe('tree');
+ expect(store.state.tree[1].type).toBe('submodule');
+ expect(store.state.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets parent tree URL', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.parentTreeUrl).toBe('parent_tree_url');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets last commit path', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.lastCommitPath).toBe('last_commit_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets root if not currently at root', (done) => {
+ store.state.isInitialRoot = false;
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.isInitialRoot).toBeTruthy();
+ expect(store.state.isRoot).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets page title', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(document.title).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles loading', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(store.state.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(store.state.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState with endpoint', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
+ const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+ store.state.prevLastCommitPath = 'test';
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
+
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let oldGetTreeData;
+ let getTreeDataSpy;
+ let tree;
+
+ beforeEach(() => {
+ getTreeDataSpy = jasmine.createSpy('getTreeData');
+
+ oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
+ store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
+
+ tree = {
+ opened: false,
+ tree: [],
+ };
+ });
+
+ afterEach(() => {
+ store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
+ });
+
+ it('toggles the tree open', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getTreeData if tree is closed', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(getTreeDataSpy).toHaveBeenCalledWith({
+ endpoint: 'test',
+ tree,
+ });
+ expect(store.state.previousUrl).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets entries tree', (done) => {
+ Object.assign(tree, {
+ opened: true,
+ tree: ['a'],
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.tree.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('pushes new state', (done) => {
+ spyOn(history, 'pushState');
+ Object.assign(tree, {
+ opened: true,
+ parentTreeUrl: 'testing',
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('clickedTreeRow', () => {
+ describe('tree', () => {
+ let toggleTreeOpenSpy;
+ let oldToggleTreeOpen;
+
+ beforeEach(() => {
+ toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
+
+ oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
+ store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
+ });
+
+ it('opens tree', (done) => {
+ const tree = {
+ url: 'a',
+ type: 'tree',
+ };
+
+ store.dispatch('clickedTreeRow', tree)
+ .then(() => {
+ expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
+ endpoint: tree.url,
+ tree,
+ });
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('submodule', () => {
+ let row;
+
+ beforeEach(() => {
+ spyOn(gl.utils, 'visitUrl');
+
+ row = {
+ url: 'submoduleurl',
+ type: 'submodule',
+ loading: false,
+ };
+ });
+
+ it('toggles loading for row', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(row.loading).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens submodule URL', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ let row;
+
+ beforeEach(() => {
+ row = {
+ type: 'blob',
+ opened: false,
+ };
+ });
+
+ it('calls getFileData', (done) => {
+ const getFileDataSpy = jasmine.createSpy('getFileData');
+ const oldGetFileData = store._actions.getFileData; // eslint-disable-line
+ store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(getFileDataSpy).toHaveBeenCalledWith(row);
+
+ store._actions.getFileData = oldGetFileData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls setFileActive when file is opened', (done) => {
+ const setFileActiveSpy = jasmine.createSpy('setFileActive');
+ const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
+ store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
+
+ row.opened = true;
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(setFileActiveSpy).toHaveBeenCalledWith(row);
+
+ store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempTree', () => {
+ it('creates temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].name).toBe('test');
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates .gitkeep file in temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('test');
+ expect(store.state.tree[0].tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tempFile).toBeUndefined();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () => Promise.resolve([{
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ }]),
+ }));
+
+ store.state.tree.push(file('testing', '1', 'tree'));
+ store.state.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates trees last commit data', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not update entry if not found', (done) => {
+ store.state.tree[0].name = 'a';
+
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('updateDirectoryData', () => {
+ it('adds data into tree', (done) => {
+ const tree = {
+ tree: [],
+ };
+ const data = {
+ trees: [{ name: 'tree' }],
+ submodules: [{ name: 'submodule' }],
+ blobs: [{ name: 'blob' }],
+ };
+
+ store.dispatch('updateDirectoryData', {
+ data,
+ tree,
+ }).then(() => {
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[0].type).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[1].type).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ expect(tree.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
new file mode 100644
index 00000000000..f2a7a698912
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -0,0 +1,419 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', (done) => {
+ store.dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeDiscardPopup', () => {
+ it('closes the discard popup', (done) => {
+ store.dispatch('closeDiscardPopup', false)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+ });
+
+ it('closes all open files', (done) => {
+ store.dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('toggleEditMode', () => {
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets preview mode', (done) => {
+ store.state.currentBlobView = 'repo-editor';
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens discard popup if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('can force closed if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('discards file changes', (done) => {
+ const f = file();
+ store.state.editMode = true;
+ store.state.tree.push(f);
+ store.state.openFiles.push(f);
+ f.changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(f.changed).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleBlobView', () => {
+ it('sets edit mode view if in edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets preview mode view if not in edit mode', (done) => {
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.project.id = 2;
+ store.state.currentBranch = 'master';
+ store.state.currentRef = '1';
+ });
+
+ it('calls service', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '1' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ let payload;
+
+ beforeEach(() => {
+ spyOn(window, 'scrollTo');
+
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.project.id = 123;
+ payload = {
+ branch: 'master',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ }));
+ });
+
+ it('calls service', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith(123, payload);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('shows flash notice', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.querySelector('.flash-notice')).not.toBeNull();
+ expect(alert.textContent.trim()).toBe(
+ 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds commit data to changed files', (done) => {
+ const changedFile = file();
+ const f = file();
+ changedFile.changed = true;
+
+ store.state.openFiles.push(changedFile, f);
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(changedFile.lastCommit.message).toBe('test message');
+ expect(f.lastCommit.message).not.toBe('test message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('closes all files', (done) => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('scrolls to top of page', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates commit ref', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.currentRef).toBe('123456');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('redirects to new merge request page', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
+
+ store.dispatch('commitChanges', { payload, newMr: true })
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ message: 'failed message',
+ }));
+ });
+
+ it('shows failed message', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe(
+ 'failed message',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempEntry', () => {
+ it('creates a temp tree', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('blob');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('popHistoryState', () => {
+
+ });
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', (done) => {
+ document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ spyOn(el, 'focus');
+
+ store.dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
new file mode 100644
index 00000000000..a204b2386cd
--- /dev/null
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -0,0 +1,119 @@
+import * as getters from '~/repo/stores/getters';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('treeList', () => {
+ it('returns flat tree list', () => {
+ localState.tree.push(file('1'));
+ localState.tree[0].tree.push(file('2'));
+ localState.tree[0].tree[0].tree.push(file('3'));
+
+ const treeList = getters.treeList(localState);
+
+ expect(treeList.length).toBe(3);
+ expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
+ expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
+ });
+ });
+
+ describe('changedFiles', () => {
+ it('returns a list of changed opened files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('changed'));
+ localState.openFiles[1].changed = true;
+
+ const changedFiles = getters.changedFiles(localState);
+
+ expect(changedFiles.length).toBe(1);
+ expect(changedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeUndefined();
+ });
+ });
+
+ describe('activeFileExtension', () => {
+ it('returns the file extension for the current active file', () => {
+ localState.openFiles.push(file('active'));
+ localState.openFiles[0].active = true;
+ localState.openFiles[0].path = 'test.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+
+ localState.openFiles[0].path = 'test.es6.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+ });
+ });
+
+ describe('isCollapsed', () => {
+ it('returns true if state has open files', () => {
+ localState.openFiles.push(file());
+
+ expect(getters.isCollapsed(localState)).toBeTruthy();
+ });
+
+ it('returns false if state has no open files', () => {
+ expect(getters.isCollapsed(localState)).toBeFalsy();
+ });
+ });
+
+ describe('canEditFile', () => {
+ beforeEach(() => {
+ localState.onTopOfBranch = true;
+ localState.canCommit = true;
+
+ localState.openFiles.push(file());
+ localState.openFiles[0].active = true;
+ });
+
+ it('returns true if user can commit and has open files', () => {
+ expect(getters.canEditFile(localState)).toBeTruthy();
+ });
+
+ it('returns false if user can commit and has no open files', () => {
+ localState.openFiles = [];
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit and active file is binary', () => {
+ localState.openFiles[0].binary = true;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user cant commit', () => {
+ localState.canCommit = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit but on a branch', () => {
+ localState.onTopOfBranch = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..3c06794d5e3
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/repo/stores/mutations/branch';
+import state from '~/repo/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranch).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..2f2835dde1f
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/file_spec.js
@@ -0,0 +1,131 @@
+import mutations from '~/repo/stores/mutations/file';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ file: localFile,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ html: 'html',
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.html).toBe('html');
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('CREATE_TMP_FILE', () => {
+ it('adds file into parent tree', () => {
+ const f = file();
+
+ mutations.CREATE_TMP_FILE(localState, {
+ file: f,
+ parent: localFile,
+ });
+
+ expect(localFile.tree.length).toBe(1);
+ expect(localFile.tree[0].name).toBe(f.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..1c76cfed9c8
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/tree_spec.js
@@ -0,0 +1,71 @@
+import mutations from '~/repo/stores/mutations/tree';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [{
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ }];
+
+ it('adds directory data', () => {
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ tree: localState,
+ });
+
+ expect(localState.tree.length).toBe(3);
+ expect(localState.tree[0].name).toBe('tree');
+ expect(localState.tree[1].name).toBe('submodule');
+ expect(localState.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('SET_PARENT_TREE_URL', () => {
+ it('sets the parent tree url', () => {
+ mutations.SET_PARENT_TREE_URL(localState, 'test');
+
+ expect(localState.parentTreeUrl).toBe('test');
+ });
+ });
+
+ describe('CREATE_TMP_TREE', () => {
+ it('adds tree into parent tree', () => {
+ const tmpEntry = file();
+
+ mutations.CREATE_TMP_TREE(localState, {
+ tmpEntry,
+ parent: localTree,
+ });
+
+ expect(localTree.tree.length).toBe(1);
+ expect(localTree.tree[0].name).toBe(tmpEntry.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
new file mode 100644
index 00000000000..d1c9885e01d
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations_spec.js
@@ -0,0 +1,117 @@
+import mutations from '~/repo/stores/mutations';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('SET_PREVIEW_MODE', () => {
+ it('sets currentBlobView to repo-preview', () => {
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+ });
+ });
+
+ describe('SET_EDIT_MODE', () => {
+ it('sets currentBlobView to repo-editor', () => {
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_EDIT_MODE', () => {
+ it('toggles editMode', () => {
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeTruthy();
+
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_DISCARD_POPUP', () => {
+ it('sets discardPopupOpen', () => {
+ mutations.TOGGLE_DISCARD_POPUP(localState, true);
+
+ expect(localState.discardPopupOpen).toBeTruthy();
+
+ mutations.TOGGLE_DISCARD_POPUP(localState, false);
+
+ expect(localState.discardPopupOpen).toBeFalsy();
+ });
+ });
+
+ describe('SET_COMMIT_REF', () => {
+ it('sets currentRef', () => {
+ mutations.SET_COMMIT_REF(localState, '123');
+
+ expect(localState.currentRef).toBe('123');
+ });
+ });
+
+ describe('SET_ROOT', () => {
+ it('sets isRoot & initialRoot', () => {
+ mutations.SET_ROOT(localState, true);
+
+ expect(localState.isRoot).toBeTruthy();
+ expect(localState.isInitialRoot).toBeTruthy();
+
+ mutations.SET_ROOT(localState, false);
+
+ expect(localState.isRoot).toBeFalsy();
+ expect(localState.isInitialRoot).toBeFalsy();
+ });
+ });
+
+ describe('SET_PREVIOUS_URL', () => {
+ it('sets previousUrl', () => {
+ mutations.SET_PREVIOUS_URL(localState, 'testing');
+
+ expect(localState.previousUrl).toBe('testing');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
new file mode 100644
index 00000000000..37287c587d7
--- /dev/null
+++ b/spec/javascripts/repo/stores/utils_spec.js
@@ -0,0 +1,102 @@
+import * as utils from '~/repo/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ describe('pushState', () => {
+ it('calls history.pushState', () => {
+ spyOn(history, 'pushState');
+
+ utils.pushState('test');
+
+ expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
+ });
+ });
+
+ describe('createTemp', () => {
+ it('creates temp tree', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'tree',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-folder');
+ });
+
+ it('creates temp file', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'blob',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-file-text-o');
+ });
+ });
+
+ describe('findIndexOfFile', () => {
+ let state;
+
+ beforeEach(() => {
+ state = [{
+ path: '1',
+ }, {
+ path: '2',
+ }];
+ });
+
+ it('finds in the index of an entry by path', () => {
+ const index = utils.findIndexOfFile(state, {
+ path: '2',
+ });
+
+ expect(index).toBe(1);
+ });
+ });
+
+ describe('findEntry', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ tree: [{
+ type: 'tree',
+ name: 'test',
+ }, {
+ type: 'blob',
+ name: 'file',
+ }],
+ };
+ });
+
+ it('returns an entry found by name', () => {
+ const foundEntry = utils.findEntry(state, 'tree', 'test');
+
+ expect(foundEntry.type).toBe('tree');
+ expect(foundEntry.name).toBe('test');
+ });
+
+ it('returns undefined when no entry found', () => {
+ const foundEntry = utils.findEntry(state, 'blob', 'test');
+
+ expect(foundEntry).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index 1a4ea2bac48..79d2c071446 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -93,7 +93,14 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
end
it 'knows it is finished for this range' do
- expect(migration.missing_members?(1, 7)).to be_falsy
+ expect(migration.missing_members?(1, 8)).to be_falsy
+ end
+
+ it 'does not miss members for forks of forks for which the root was deleted' do
+ forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id)
+ base1.destroy
+
+ expect(migration.missing_members?(7, 10)).to be_falsy
end
context 'with more forks' do
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 74a24a4424b..c2bca816aae 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -165,47 +165,16 @@ describe Gitlab::Checks::ChangeAccess do
end
context 'LFS integrity check' do
- let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+ it 'fails if any LFS blobs are missing' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true)
- before do
- allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block|
- lazy_block.call([blob_object.id])
- end
- end
-
- context 'with LFS not enabled' do
- it 'skips integrity check' do
- expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
-
- subject.exec
- end
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
end
- context 'with LFS enabled' do
- before do
- allow(project).to receive(:lfs_enabled?).and_return(true)
- end
-
- context 'deletion' do
- let(:changes) { { oldrev: oldrev, ref: ref } }
-
- it 'skips integrity check' do
- expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+ it 'succeeds if LFS objects have already been uploaded' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false)
- subject.exec
- end
- end
-
- it 'fails if any LFS blobs are missing' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
- end
-
- it 'succeeds if LFS objects have already been uploaded' do
- lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
- create(:lfs_objects_project, project: project, lfs_object: lfs_object)
-
- expect { subject.exec }.not_to raise_error
- end
+ expect { subject.exec }.not_to raise_error
end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
new file mode 100644
index 00000000000..17756621221
--- /dev/null
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::LfsIntegrity do
+ include ProjectForksHelper
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ subject { described_class.new(project, newrev) }
+
+ describe '#objects_missing?' do
+ let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block|
+ lazy_block.call([blob_object.id])
+ end
+ end
+
+ context 'with LFS not enabled' do
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ subject.objects_missing?
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ context 'deletion' do
+ let(:newrev) { nil }
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ it 'is true if any LFS blobs are missing' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false if LFS objects have already been uploaded' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ context 'for forked project' do
+ let(:parent_project) { create(:project, :repository) }
+ let(:project) { fork_project(parent_project, nil, repository: true) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ it 'is true parent project is missing LFS objects' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false parent project already conatins LFS objects for the fork' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
new file mode 100644
index 00000000000..0506210887c
--- /dev/null
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RemoteRepository, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ subject { described_class.new(repository) }
+
+ describe '#empty_repo?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:repository, :result) do
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true
+ end
+
+ with_them do
+ it { expect(subject.empty_repo?).to eq(result) }
+ end
+ end
+
+ describe '#commit_id' do
+ it 'returns an OID if the revision exists' do
+ expect(subject.commit_id('v1.0.0')).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+
+ it 'is nil when the revision does not exist' do
+ expect(subject.commit_id('does-not-exist')).to be_nil
+ end
+ end
+
+ describe '#branch_exists?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch, :result) do
+ 'master' | true
+ 'does-not-exist' | false
+ end
+
+ with_them do
+ it { expect(subject.branch_exists?(branch)).to eq(result) }
+ end
+ end
+
+ describe '#same_repository?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:other_repository, :result) do
+ repository | true
+ Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true
+ Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false
+ Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false
+ end
+
+ with_them do
+ it { expect(subject.same_repository?(other_repository)).to eq(result) }
+ end
+ end
+
+ describe '#fetch_env' do
+ let(:remote_repository) { described_class.new(repository) }
+
+ let(:gitaly_client) { double(:gitaly_client) }
+ let(:address) { 'fake-address' }
+ let(:token) { 'fake-token' }
+
+ subject { remote_repository.fetch_env }
+
+ before do
+ allow(remote_repository).to receive(:gitaly_client).and_return(gitaly_client)
+
+ expect(gitaly_client).to receive(:address).with(repository.storage).and_return(address)
+ expect(gitaly_client).to receive(:token).with(repository.storage).and_return(token)
+ end
+
+ it { expect(subject).to be_a(Hash) }
+ it { expect(subject['GITALY_ADDRESS']).to eq(address) }
+ it { expect(subject['GITALY_TOKEN']).to eq(token) }
+ it { expect(subject['GITALY_WD']).to eq(Dir.pwd) }
+
+ it 'creates a plausible GIT_SSH_COMMAND' do
+ git_ssh_command = subject['GIT_SSH_COMMAND']
+
+ expect(git_ssh_command).to start_with('/')
+ expect(git_ssh_command).to end_with('/gitaly-ssh upload-pack')
+ end
+
+ it 'creates a plausible GITALY_PAYLOAD' do
+ req = Gitaly::SSHUploadPackRequest.decode_json(subject['GITALY_PAYLOAD'])
+
+ expect(remote_repository.gitaly_repository).to eq(req.repository)
+ end
+
+ context 'when the token is blank' do
+ let(:token) { '' }
+
+ it { expect(subject.keys).not_to include('GITALY_TOKEN') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index ee14b528ec2..5d990b42c24 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -449,7 +449,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -484,7 +483,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -544,7 +542,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
@@ -570,7 +567,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
@@ -588,7 +584,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
@@ -1122,7 +1117,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1169,7 +1163,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1419,7 +1412,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1537,35 +1529,60 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#fetch_source_branch!' do
- let(:local_ref) { 'refs/merge-requests/1/head' }
+ shared_examples '#fetch_source_branch!' do
+ let(:local_ref) { 'refs/merge-requests/1/head' }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
- context 'when the branch exists' do
- let(:source_branch) { 'master' }
+ after do
+ ensure_seeds
+ end
- it 'writes the ref' do
- expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/)
+ context 'when the branch exists' do
+ context 'when the commit does not exist locally' do
+ let(:source_branch) { 'new-branch-for-fetch-source-branch' }
+ let(:source_rugged) { source_repository.rugged }
+ let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
- repository.fetch_source_branch!(repository, source_branch, local_ref)
- end
+ before do
+ source_rugged.branches.create(source_branch, new_oid)
+ end
- it 'returns true' do
- expect(repository.fetch_source_branch!(repository, source_branch, local_ref)).to eq(true)
- end
- end
+ it 'writes the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(new_oid)
+ end
+ end
- context 'when the branch does not exist' do
- let(:source_branch) { 'definitely-not-master' }
+ context 'when the commit exists locally' do
+ let(:source_branch) { 'master' }
+ let(:expected_oid) { SeedRepo::LastCommit::ID }
- it 'does not write the ref' do
- expect(repository).not_to receive(:write_ref)
+ it 'writes the ref' do
+ # Sanity check: the commit should already exist
+ expect(repository.commit(expected_oid)).not_to be_nil
- repository.fetch_source_branch!(repository, source_branch, local_ref)
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(expected_oid)
+ end
+ end
end
- it 'returns false' do
- expect(repository.fetch_source_branch!(repository, source_branch, local_ref)).to eq(false)
+ context 'when the branch does not exist' do
+ let(:source_branch) { 'definitely-not-master' }
+
+ it 'does not write the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false)
+ expect(repository.commit(local_ref)).to be_nil
+ end
end
end
+
+ it_behaves_like '#fetch_source_branch!'
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#fetch_source_branch!'
+ end
end
describe '#rm_branch' do
@@ -1641,7 +1658,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
new file mode 100644
index 00000000000..6ad9f5ef766
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::WikiService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:client) { described_class.new(project.repository) }
+ let(:commit) { create(:gitaly_commit) }
+ let(:page_version) { Gitaly::WikiPageVersion.new(format: 'markdown', commit: commit) }
+ let(:page_info) { { title: 'My Page', raw_data: 'a', version: page_version } }
+
+ describe '#find_page' do
+ let(:response) do
+ [
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b'))
+ ]
+ end
+ let(:wiki_page) { subject.first }
+ let(:wiki_page_version) { subject.last }
+
+ subject { client.find_page(title: 'My Page', version: 'master', dir: '') }
+
+ it 'sends a wiki_find_page message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(wiki_page.title).to eq('My Page')
+ expect(wiki_page.raw_data).to eq('ab')
+ expect(wiki_page_version.format).to eq('markdown')
+ end
+ end
+
+ describe '#get_all_pages' do
+ let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
+ let(:response) do
+ [
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_2_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'd')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true)
+ ]
+ end
+ let(:wiki_page_1) { subject[0].first }
+ let(:wiki_page_1_version) { subject[0].last }
+ let(:wiki_page_2) { subject[1].first }
+ let(:wiki_page_2_version) { subject[1].last }
+
+ subject { client.get_all_pages }
+
+ it 'sends a wiki_get_all_pages message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion for each page' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(subject.size).to be(2)
+ expect(wiki_page_1.title).to eq('My Page')
+ expect(wiki_page_1.raw_data).to eq('ab')
+ expect(wiki_page_1_version.format).to eq('markdown')
+ expect(wiki_page_2.title).to eq('My Page 2')
+ expect(wiki_page_2.raw_data).to eq('cd')
+ expect(wiki_page_2_version.format).to eq('markdown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 6c529cdd051..aeacd577d18 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::HookData::IssueBuilder do
%w[
assignee_id
author_id
- branch_name
closed_at
confidential
created_at
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 6d69b5305d2..ae1d8b47fe9 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -4,32 +4,40 @@ describe Gitlab::Metrics::SidekiqMiddleware do
let(:middleware) { described_class.new }
let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } }
- def run(worker, message)
- expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
- .with(worker.class)
- .and_call_original
-
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
- .with(:sidekiq_queue_duration, instance_of(Float))
+ describe '#call' do
+ it 'tracks the transaction' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
+ .and_call_original
- middleware.call(worker, message, :test) { nil }
- end
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
+ .with(:sidekiq_queue_duration, instance_of(Float))
- describe '#call' do
- let(:test_worker_class) { double(:class, name: 'TestWorker') }
- let(:worker) { double(:worker, class: test_worker_class) }
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
- it 'tracks the transaction' do
- run(worker, message)
+ middleware.call(worker, message, :test) { nil }
end
it 'tracks the transaction (for messages without `enqueued_at`)' do
- run(worker, {})
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
+ .and_call_original
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
+ .with(:sidekiq_queue_duration, instance_of(Float))
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+
+ middleware.call(worker, {}, :test) { nil }
end
it 'tracks any raised exceptions' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
expect_any_instance_of(Gitlab::Metrics::Transaction)
.to receive(:run).and_raise(RuntimeError)
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
new file mode 100644
index 00000000000..4a104ab6d97
--- /dev/null
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::Utils::StrongMemoize do
+ let(:klass) do
+ struct = Struct.new(:value) do
+ def method_name
+ strong_memoize(:method_name) do
+ trace << value
+ value
+ end
+ end
+
+ def trace
+ @trace ||= []
+ end
+ end
+
+ struct.include(described_class)
+ struct
+ end
+
+ subject(:object) { klass.new(value) }
+
+ shared_examples 'caching the value' do
+ it 'only calls the block once' do
+ value0 = object.method_name
+ value1 = object.method_name
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(object.trace).to contain_exactly(value)
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.method_name
+ memoized_value = object.instance_variable_get(:@method_name)
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql(value)
+ end
+ end
+
+ describe '#strong_memoize' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+
+ it_behaves_like 'caching the value'
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index c832cee965b..f942a22b6d1 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -783,7 +783,25 @@ describe Notify do
shared_examples 'an email for a note on a diff discussion' do |model|
let(:note) { create(model, author: note_author) }
- it "includes diffs with character-level highlighting" do
+ context 'when note is on image' do
+ before do
+ allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true)
+ end
+
+ it 'does not include diffs with character-level highlighting' do
+ is_expected.not_to have_body_text '<span class="p">}</span></span>'
+ end
+
+ it 'ends the intro with a dot' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>."
+ end
+ end
+
+ it 'ends the intro with a colon' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>:"
+ end
+
+ it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text '<span class="p">}</span></span>'
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5ed2e1ca99a..1795ee8e9a4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -270,6 +270,23 @@ describe Ci::Build do
end
end
+ describe '#triggered_by?' do
+ subject { build.triggered_by?(user) }
+
+ context 'when user is owner' do
+ let(:build) { create(:ci_build, pipeline: pipeline, user: user) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user is not owner' do
+ let(:another_user) { create(:user) }
+ let(:build) { create(:ci_build, pipeline: pipeline, user: another_user) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#detailed_status' do
it 'returns a detailed status' do
expect(build.detailed_status(user))
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
new file mode 100644
index 00000000000..cbdc438be0b
--- /dev/null
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Avatarable do
+ subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+
+ let(:gitlab_host) { "https://gitlab.example.com" }
+ let(:relative_url_root) { "/gitlab" }
+ let(:asset_host) { "https://gitlab-assets.example.com" }
+
+ before do
+ stub_config_setting(base_url: gitlab_host)
+ stub_config_setting(relative_url_root: relative_url_root)
+ end
+
+ describe '#avatar_path' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do
+ true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PUBLIC | true | [subject.avatar.url]
+ true | Project::PUBLIC | false | [asset_host, subject.avatar.url]
+ false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url]
+ false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ end
+
+ with_them do
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil)
+ subject.visibility_level = visibility_level
+ end
+
+ it 'returns the expected avatar path' do
+ expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 66353935427..9048da0c73d 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do
expect(milestone.elapsed_days).to eq(2)
end
end
+
+ describe '#total_issue_time_spent' do
+ it 'calculates total issue time spent' do
+ closed_issue_1.spend_time(duration: 300, user: author)
+ closed_issue_1.save!
+ closed_issue_2.spend_time(duration: 600, user: assignee)
+ closed_issue_2.save!
+
+ expect(milestone.total_issue_time_spent).to eq(900)
+ end
+ end
+
+ describe '#human_total_issue_time_spent' do
+ it 'returns nil if no time has been spent' do
+ expect(milestone.human_total_issue_time_spent).to be_nil
+ end
+ end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index da972d2d86a..8389d5c5430 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -283,6 +283,12 @@ describe DiffNote do
expect(diff_line).to be nil
expect(subject).to be_valid
end
+
+ it "does not update the position" do
+ expect(subject).not_to receive(:update_position)
+
+ subject.save
+ end
end
it "returns true for on_image?" do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index d4052a64570..5e82a2988ce 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -247,8 +247,6 @@ describe Group do
describe '#avatar_url' do
let!(:group) { create(:group, :access_requestable, :with_avatar) }
let(:user) { create(:user) }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/group/avatar/#{group.id}/dk.png" }
context 'when avatar file is uploaded' do
before do
@@ -256,12 +254,8 @@ describe Group do
end
it 'shows correct avatar url' do
- expect(group.avatar_url).to eq(avatar_path)
- expect(group.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(group.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(group.avatar_url).to eq(group.avatar.url)
+ expect(group.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, group.avatar.url].join)
end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bb5033c1628..5f901262598 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -765,22 +765,4 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue])
end
end
-
- describe '#update_project_counter_caches?' do
- it 'returns true when the state changes' do
- subject.state = 'closed'
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns true when the confidential flag changes' do
- subject.confidential = true
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns false when the state or confidential flag did not change' do
- expect(subject.update_project_counter_caches?).to eq(false)
- end
- end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d022dae3476..d250ad50713 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1772,16 +1772,4 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
-
- describe '#update_project_counter_caches?' do
- it 'returns true when the state changes' do
- subject.state = 'closed'
-
- expect(subject.update_project_counter_caches?).to eq(true)
- end
-
- it 'returns false when the state did not change' do
- expect(subject.update_project_counter_caches?).to eq(false)
- end
- end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e684389af5d..f7f19d464d1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -883,20 +883,14 @@ describe Project do
context 'when avatar file is uploaded' do
let(:project) { create(:project, :public, :with_avatar) }
- let(:avatar_path) { "/uploads/-/system/project/avatar/#{project.id}/dk.png" }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
it 'shows correct url' do
- expect(project.avatar_url).to eq(avatar_path)
- expect(project.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(project.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(project.avatar_url).to eq(project.avatar.url)
+ expect(project.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, project.avatar.url].join)
end
end
- context 'When avatar file in git' do
+ context 'when avatar file in git' do
before do
allow(project).to receive(:avatar_in_git) { true }
end
@@ -1944,6 +1938,24 @@ describe Project do
expect(second_fork.fork_source).to eq(project)
end
end
+
+ describe '#lfs_storage_project' do
+ it 'returns self for non-forks' do
+ expect(project.lfs_storage_project).to eq project
+ end
+
+ it 'returns the fork network root for forks' do
+ second_fork = fork_project(forked_project)
+
+ expect(second_fork.lfs_storage_project).to eq project
+ end
+
+ it 'returns self when fork_source is nil' do
+ expect(forked_project).to receive(:fork_source).and_return(nil)
+
+ expect(forked_project.lfs_storage_project).to eq forked_project
+ end
+ end
end
describe '#pushes_since_gc' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a3ebf649aa8..65dccf9638d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1149,16 +1149,9 @@ describe User do
let(:user) { create(:user, :with_avatar) }
context 'when avatar file is uploaded' do
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/user/avatar/#{user.id}/dk.png" }
-
it 'shows correct avatar url' do
- expect(user.avatar_url).to eq(avatar_path)
- expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(user.avatar_url).to eq(user.avatar.url)
+ expect(user.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, user.avatar.url].join)
end
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 8e1bc3d1543..298a9d16425 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -150,5 +150,82 @@ describe Ci::BuildPolicy do
end
end
end
+
+ describe 'rules for erase build' do
+ let(:project) { create(:project, :repository) }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: 'some-ref', user: owner) }
+
+ context 'when a developer erases a build' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when developers can push to the branch' do
+ before do
+ create(:protected_branch, :developers_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when a master erases a build' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when masters can push to the branch' do
+ before do
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the master' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 780dbce6488..04a658cd6c3 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -427,6 +427,142 @@ describe API::Groups do
end
end
+ describe 'GET /groups/:id/subgroups', :nested_groups do
+ let!(:subgroup1) { create(:group, parent: group1) }
+ let!(:subgroup2) { create(:group, :private, parent: group1) }
+ let!(:subgroup3) { create(:group, :private, parent: group2) }
+
+ context 'when unauthenticated' do
+ it 'returns only public subgroups' do
+ get api("/groups/#{group1.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(subgroup1.id)
+ expect(json_response.first['parent_id']).to eq(group1.id)
+ end
+
+ it 'returns 404 for a private group' do
+ get api("/groups/#{group2.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated as user' do
+ context 'when user is not member of a public group' do
+ it 'returns no subgroups for the public group' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ context 'when using all_available in request' do
+ it 'returns public subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2), all_available: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['id']).to eq(subgroup1.id)
+ expect(json_response[0]['parent_id']).to eq(group1.id)
+ end
+ end
+ end
+
+ context 'when user is not member of a private group' do
+ it 'returns 404 for the private group' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user is member of public group' do
+ before do
+ group1.add_guest(user2)
+ end
+
+ it 'returns private subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ private_subgroups = json_response.select { |group| group['visibility'] == 'private' }
+ expect(private_subgroups.length).to eq(1)
+ expect(private_subgroups.first['id']).to eq(subgroup2.id)
+ expect(private_subgroups.first['parent_id']).to eq(group1.id)
+ end
+
+ context 'when using statistics in request' do
+ it 'does not include statistics' do
+ get api("/groups/#{group1.id}/subgroups", user2), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
+ end
+
+ context 'when user is member of private group' do
+ before do
+ group2.add_guest(user1)
+ end
+
+ it 'returns subgroups' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(subgroup3.id)
+ expect(json_response.first['parent_id']).to eq(group2.id)
+ end
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns private subgroups of a public group' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns subgroups of a private group' do
+ get api("/groups/#{group2.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+
+ it 'does not include statistics by default' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it 'includes statistics if requested' do
+ get api("/groups/#{group1.id}/subgroups", admin), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include('statistics')
+ end
+ end
+ end
+
describe "POST /groups" do
context "when authenticated as user without group permissions" do
it "does not create group" do
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 1765907c1b4..2a83213e87a 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -500,7 +500,11 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/erase' do
+ let(:role) { :master }
+
before do
+ project.team << [user, role]
+
post api("/projects/#{project.id}/jobs/#{job.id}/erase", user)
end
@@ -529,6 +533,23 @@ describe API::Jobs do
expect(response).to have_gitlab_http_status(403)
end
end
+
+ context 'when a developer erases a build' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) }
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(response).to have_gitlab_http_status(201) }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(response).to have_gitlab_http_status(403) }
+ end
+ end
end
describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index d13b3a958c9..d412b045e9f 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe API::PagesDomains do
set(:project) { create(:project) }
set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
@@ -23,12 +24,49 @@ describe API::PagesDomains do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end
+ describe 'GET /pages/domains' do
+ context 'when pages is disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api('/pages/domains', admin) }
+ end
+ end
+
+ context 'when pages is enabled' do
+ context 'when authenticated as an admin' do
+ it 'returns paginated all pages domains' do
+ get api('/pages/domains', admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.last).to have_key('domain')
+ expect(json_response.last).to have_key('certificate_expiration')
+ expect(json_response.last['certificate_expiration']['expired']).to be true
+ expect(json_response.first).not_to have_key('certificate_expiration')
+ end
+ end
+
+ context 'when authenticated as a non-member' do
+ it_behaves_like '403 response' do
+ let(:request) { get api('/pages/domains', user) }
+ end
+ end
+ end
+ end
+
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do
get api(route, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domains')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
@@ -99,6 +137,7 @@ describe API::PagesDomains do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil
@@ -108,6 +147,7 @@ describe API::PagesDomains do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
@@ -118,6 +158,7 @@ describe API::PagesDomains do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['certificate']['expired']).to be true
end
end
@@ -187,6 +228,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
@@ -197,6 +239,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
@@ -270,6 +313,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
end
@@ -279,6 +323,7 @@ describe API::PagesDomains do
pages_domain.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
end
@@ -288,6 +333,7 @@ describe API::PagesDomains do
pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
@@ -297,6 +343,7 @@ describe API::PagesDomains do
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index dfe48e45d49..ba697e2b305 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -175,4 +175,25 @@ describe API::Services do
end
end
end
+
+ describe 'Mattermost service' do
+ let(:service_name) { 'mattermost' }
+ let(:params) do
+ { webhook: 'https://hook.example.com', username: 'username' }
+ end
+
+ before do
+ project.create_mattermost_service(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts a username for update' do
+ put api("/projects/#{project.id}/services/mattermost", user), params.merge(username: 'new_username')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['properties']['username']).to eq('new_username')
+ end
+ end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 3f58b7ef384..a73bb456b52 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -408,6 +408,8 @@ describe API::V3::Builds do
describe 'POST /projects/:id/builds/:build_id/erase' do
before do
+ project.add_master(user)
+
post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 214adc9960f..0ce41e7c7ee 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -292,6 +292,30 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
+ context 'when there is only one manual action' do
+ before do
+ create_build('deploy', stage_idx: 0, when: 'manual', allow_failure: true)
+
+ process_pipeline
+ end
+
+ it 'skips the pipeline' do
+ expect(pipeline.reload).to be_skipped
+ end
+
+ context 'when the action was played' do
+ before do
+ play_manual_action('deploy')
+ end
+
+ it 'queues the action and pipeline' do
+ expect(all_builds_statuses).to eq(%w[pending])
+
+ expect(pipeline.reload).to be_pending
+ end
+ end
+ end
+
context 'when blocking manual actions are defined' do
before do
create_build('code:test', stage_idx: 0)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ac196e92601..f86f1ac2443 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -248,6 +248,28 @@ describe MergeRequests::MergeService do
expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+
+ context "when fast-forward merge is not allowed" do
+ before do
+ allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
+ end
+
+ %w(semi-linear ff).each do |merge_method|
+ it "logs and saves error if merge is #{merge_method} only" do
+ merge_method = 'rebase_merge' if merge_method == 'semi-linear'
+ merge_request.project.update(merge_method: merge_method)
+ error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request).to be_open
+ expect(merge_request.merge_commit_sha).to be_nil
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 5739386dd0d..16e288b3148 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -4,7 +4,7 @@ describe Milestones::DestroyService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
- let(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 3a569cf7c19..00000000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "compilerOptions": {
- "target": "es5",
- "strict": true,
- "module": "es2015",
- "moduleResolution": "node"
- }
-} \ No newline at end of file
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
deleted file mode 100644
index da37966b28a..00000000000
--- a/vendor/assets/javascripts/latinise.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Converting text to basic latin (aka removing accents)
-//
-// Based on: http://semplicewebsites.com/removing-accents-javascript
-//
-var Latinise = {
- map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"}
-};
-
-String.prototype.latinise = function() {
- return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
-};
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index c79ba5080a3..addf405e4f5 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -32,7 +32,7 @@ proguard/
# Android Studio captures folder
captures/
-# Intellij
+# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index 9bf1537f6ae..ecf66f84291 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -24,7 +24,7 @@ Build.bat
# Module::Install
inc/
-# ExtUitls::MakeMaker
+# ExtUtils::MakeMaker
/blib/
/_eumm/
/*.gz
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index 02cfab3a5b2..36386a19fdc 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -16,7 +16,7 @@ image: "crystallang/crystal:latest"
# Cache shards in between builds
cache:
paths:
- - libs
+ - lib
# This is a basic example for a shard or script which doesn't use
# services such as redis or postgres
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 9f78059986d..6f6ca5f8b32 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,7 +1,11 @@
+"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD"
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
+abbrev,1.1.0,ISC
accepts,1.3.3,MIT
ace-rails-ap,4.1.2,MIT
+acorn,3.3.0,MIT
+acorn,4.0.13,MIT
acorn,5.1.1,MIT
acorn-dynamic-import,2.0.2,MIT
acorn-jsx,3.0.1,MIT
@@ -15,7 +19,9 @@ activesupport,4.2.8,MIT
acts-as-taggable-on,4.0.0,MIT
addressable,2.5.2,Apache 2.0
after,0.8.2,MIT
+ajv,4.11.8,MIT
ajv,5.2.2,MIT
+ajv-keywords,1.5.1,MIT
ajv-keywords,2.1.0,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
@@ -24,10 +30,14 @@ alphanum-sort,1.0.2,MIT
amdefine,1.0.1,BSD-3-Clause OR MIT
ansi-escapes,1.4.0,MIT
ansi-html,0.0.5,"Apache, Version 2.0"
+ansi-html,0.0.7,Apache 2.0
ansi-regex,2.1.1,MIT
ansi-styles,2.2.1,MIT
+ansi-styles,3.2.0,MIT
anymatch,1.3.2,ISC
append-transform,0.4.0,MIT
+aproba,1.1.1,ISC
+are-we-there-yet,1.1.4,ISC
arel,6.0.4,MIT
argparse,1.0.9,MIT
arr-diff,2.0.0,MIT
@@ -35,6 +45,7 @@ arr-flatten,1.0.1,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
+array-flatten,2.1.1,MIT
array-slice,0.2.3,MIT
array-union,1.0.2,MIT
array-uniq,1.0.3,MIT
@@ -44,15 +55,24 @@ arrify,1.0.1,MIT
asana,0.6.0,MIT
asciidoctor,1.5.3,MIT
asciidoctor-plantuml,0.0.7,MIT
+asn1,0.2.3,MIT
asn1.js,4.9.1,MIT
assert,1.4.1,MIT
+assert-plus,0.2.0,MIT
+assert-plus,1.0.0,MIT
+async,0.9.2,MIT
+async,1.5.2,MIT
async,2.4.1,MIT
async-each,1.0.1,MIT
+asynckit,0.4.0,MIT
atomic,1.1.99,Apache 2.0
attr_encrypted,3.0.3,MIT
attr_required,1.0.0,MIT
autoprefixer,6.7.7,MIT
autoprefixer-rails,6.2.3,MIT
+autosize,4.0.0,MIT
+aws-sign2,0.6.0,Apache 2.0
+aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
axios,0.16.2,MIT
babel-code-frame,6.22.0,MIT
@@ -130,6 +150,7 @@ babel-types,6.23.0,MIT
babosa,1.0.2,MIT
babylon,6.16.1,MIT
backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
balanced-match,1.0.0,MIT
base32,0.3.2,MIT
base64-arraybuffer,0.1.5,MIT
@@ -137,19 +158,25 @@ base64-js,1.2.0,MIT
base64id,1.0.0,MIT
batch,0.6.1,MIT
bcrypt,3.1.11,MIT
+bcrypt-pbkdf,1.0.1,New BSD
bcrypt_pbkdf,1.0.0,MIT
better-assert,1.0.2,MIT
big.js,3.1.3,MIT
binary-extensions,1.10.0,MIT
bindata,2.4.1,ruby
blob,0.0.4,unknown
+block-stream,0.0.9,ISC
bluebird,2.11.0,MIT
+bluebird,3.5.0,MIT
bn.js,4.11.6,MIT
body-parser,1.17.2,MIT
bonjour,3.5.0,MIT
+boom,2.10.1,New BSD
bootstrap-sass,3.3.6,MIT
bootstrap_form,2.7.0,MIT
+brace-expansion,1.1.7,MIT
brace-expansion,1.1.8,MIT
+braces,0.1.5,MIT
braces,1.8.5,MIT
brorand,1.0.7,MIT
browser,2.2.0,MIT
@@ -168,17 +195,23 @@ builder,3.2.3,MIT
builtin-modules,1.1.1,MIT
builtin-status-codes,3.0.0,MIT
bytes,2.4.0,MIT
+bytes,2.5.0,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,unknown
callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+camelcase,2.1.1,MIT
+camelcase,3.0.0,MIT
camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
caniuse-db,1.0.30000649,CC-BY-4.0
-carrierwave,1.1.0,MIT
+carrierwave,1.2.1,MIT
+caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
chalk,1.1.3,MIT
+chalk,2.3.0,MIT
charlock_holmes,0.7.5,MIT
chokidar,1.7.0,MIT
chronic,0.10.2,MIT
@@ -191,6 +224,7 @@ clap,1.1.3,MIT
cli-cursor,1.0.2,MIT
cli-width,2.1.0,ISC
clipboard,1.6.1,MIT
+cliui,2.1.0,ISC
cliui,3.2.0,ISC
clone,1.0.2,MIT
co,4.6.0,MIT
@@ -204,9 +238,11 @@ color-string,0.3.0,MIT
colormin,1.1.2,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
+combined-stream,1.0.5,MIT
commander,2.9.0,MIT
commondir,1.0.1,MIT
component-bind,1.0.0,unknown
+component-emitter,1.1.2,unknown
component-emitter,1.2.1,MIT
component-inherit,0.0.3,unknown
compressible,2.0.11,MIT
@@ -220,7 +256,8 @@ configstore,1.4.0,Simplified BSD
connect,3.6.3,MIT
connect-history-api-fallback,1.3.0,MIT
connection_pool,2.2.1,MIT
-console-browserify,1.1.0,[Circular]
+console-browserify,1.1.0,MIT
+console-control-strings,1.1.0,ISC
consolidate,0.14.5,MIT
constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
@@ -230,6 +267,7 @@ convert-source-map,1.3.0,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-webpack-plugin,4.0.1,MIT
+core-js,2.3.0,MIT
core-js,2.4.1,MIT
core-util-is,1.0.2,MIT
cosmiconfig,2.1.1,MIT
@@ -240,9 +278,11 @@ create-hmac,1.1.4,MIT
creole,0.5.0,ruby
cropper,2.3.0,MIT
cross-spawn,5.1.0,MIT
+cryptiles,2.0.5,New BSD
crypto-browserify,3.11.0,MIT
css-color-names,0.0.4,MIT
css-loader,0.28.0,MIT
+css-selector-tokenizer,0.6.0,MIT
css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
@@ -250,11 +290,16 @@ cssnano,3.10.0,MIT
csso,2.3.2,MIT
currently-unhandled,0.4.1,MIT
custom-event,1.0.1,MIT
+d,0.1.1,MIT
d,1.0.0,MIT
d3,3.5.11,New BSD
d3_rails,3.5.11,MIT
+dashdash,1.14.1,MIT
date-now,0.1.4,MIT
de-indent,1.0.2,MIT
+debug,2.2.0,MIT
+debug,2.3.3,MIT
+debug,2.6.7,MIT
debug,2.6.8,MIT
debugger-ruby_core_source,1.3.8,MIT
decamelize,1.2.0,MIT
@@ -269,7 +314,11 @@ default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
defined,1.0.0,MIT
del,2.2.2,MIT
+del,3.0.0,MIT
+delayed-stream,1.0.0,MIT
delegate,3.1.2,MIT
+delegates,1.0.0,MIT
+depd,1.1.0,MIT
depd,1.1.1,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
@@ -285,12 +334,14 @@ diffy,3.1.0,MIT
dns-equal,1.0.0,MIT
dns-packet,1.2.2,MIT
dns-txt,2.0.2,MIT
+doctrine,1.5.0,BSD
doctrine,2.0.0,Apache 2.0
document-register-element,1.3.0,MIT
dom-serialize,2.2.1,MIT
dom-serializer,0.1.0,MIT
domain-browser,1.1.7,MIT
domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+domelementtype,1.1.3,unknown
domelementtype,1.3.0,unknown
domhandler,2.3.0,unknown
domutils,1.5.1,unknown
@@ -298,9 +349,10 @@ doorkeeper,4.2.6,MIT
doorkeeper-openid_connect,1.2.0,MIT
dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
-duplexer,0.1.1,[Circular]
+duplexer,0.1.1,MIT
duplexer3,0.1.4,New BSD
duplexify,3.5.1,MIT
+ecc-jsbn,0.1.1,MIT
editorconfig,0.13.2,MIT
ee-first,1.1.1,MIT
ejs,2.5.6,Apache 2.0
@@ -315,6 +367,7 @@ end-of-stream,1.4.0,MIT
engine.io,1.8.3,MIT
engine.io-client,1.8.3,MIT
engine.io-parser,1.3.2,MIT
+enhanced-resolve,0.9.1,MIT
enhanced-resolve,3.4.1,MIT
ent,2.2.0,MIT
entities,1.1.1,BSD-like
@@ -346,9 +399,12 @@ eslint-plugin-jasmine,2.2.0,MIT
eslint-plugin-promise,3.5.0,ISC
espree,3.5.0,Simplified BSD
esprima,2.7.3,Simplified BSD
+esprima,4.0.0,Simplified BSD
esquery,1.0.0,BSD
esrecurse,4.1.0,Simplified BSD
+estraverse,1.9.3,BSD
estraverse,4.1.1,Simplified BSD
+estraverse,4.2.0,Simplified BSD
esutils,2.0.2,BSD
et-orbi,1.0.3,MIT
etag,1.8.0,MIT
@@ -365,12 +421,14 @@ execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
expand-braces,0.1.2,MIT
expand-brackets,0.1.5,MIT
+expand-range,0.1.1,MIT
expand-range,1.8.2,MIT
exports-loader,0.6.4,MIT
express,4.15.4,MIT
expression_parser,0.9.0,MIT
extend,3.0.1,MIT
extglob,0.3.2,MIT
+extsprintf,1.0.2,MIT
faraday,0.12.2,MIT
faraday_middleware,0.11.0.1,MIT
faraday_middleware-multi_json,0.0.6,MIT
@@ -378,6 +436,8 @@ fast-deep-equal,1.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_gettext,1.4.0,"MIT,ruby"
fastparse,1.1.1,MIT
+faye-websocket,0.10.0,MIT
+faye-websocket,0.11.1,MIT
faye-websocket,0.7.3,MIT
ffi,1.9.18,New BSD
figures,1.7.0,MIT
@@ -386,17 +446,19 @@ file-loader,0.11.1,MIT
filename-regex,2.0.0,MIT
fileset,2.0.3,MIT
filesize,3.3.0,New BSD
+filesize,3.5.10,New BSD
fill-range,2.2.3,MIT
finalhandler,1.0.4,MIT
find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
+find-up,1.1.2,MIT
find-up,2.1.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.10.2,MIT
flipper-active_record,0.10.2,MIT
flowdock,0.7.1,MIT
-fog-aliyun,0.1.0,MIT
+fog-aliyun,0.2.0,MIT
fog-aws,1.4.0,MIT
fog-core,1.44.3,MIT
fog-google,0.5.3,MIT
@@ -409,6 +471,8 @@ follow-redirects,1.2.3,MIT
font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License"
for-in,0.1.6,MIT
for-own,0.1.4,MIT
+forever-agent,0.6.1,Apache 2.0
+form-data,2.1.4,MIT
formatador,0.2.5,MIT
forwarded,0.1.0,MIT
fresh,0.5.0,MIT
@@ -416,8 +480,12 @@ from,0.1.7,MIT
fs-access,1.0.1,MIT
fs-extra,0.26.7,MIT
fs.realpath,1.0.0,ISC
-fsevents,,unknown
+fsevents,1.1.2,MIT
+fstream,1.0.11,ISC
+fstream-ignore,1.0.5,ISC
function-bind,1.1.0,MIT
+fuzzaldrin-plus,0.5.0,MIT
+gauge,2.7.4,ISC
gemnasium-gitlab-service,0.2.6,MIT
gemojione,3.3.0,MIT
generate-function,2.0.0,MIT
@@ -426,30 +494,37 @@ get-caller-file,1.0.2,ISC
get-stdin,4.0.1,MIT
get-stream,3.0.0,MIT
get_process_mem,0.2.0,MIT
+getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.2.0,MIT
-gitaly-proto,0.41.0,MIT
+gitaly-proto,0.51.0,MIT
github-linguist,4.7.6,MIT
github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
gitlab-grit,2.8.2,MIT
-gitlab-markup,1.6.2,MIT
+gitlab-markup,1.6.3,MIT
gitlab-svgs,1.0.4,unknown
gitlab_omniauth-ldap,2.0.4,MIT
+glob,5.0.15,ISC
glob,6.0.4,ISC
+glob,7.1.1,ISC
+glob,7.1.2,ISC
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
globalid,0.3.7,MIT
globals,9.18.0,MIT
globby,5.0.0,MIT
+globby,6.1.0,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.13.6,Apache 2.0
-google-protobuf,3.4.0.2,New BSD
+google-protobuf,3.4.1.1,New BSD
+googleapis-common-protos-types,1.0.0,Apache 2.0
googleauth,0.5.3,Apache 2.0
+got,3.3.1,MIT
got,7.1.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
@@ -458,25 +533,31 @@ grape,1.0.0,MIT
grape-entity,0.6.0,MIT
grape-route-helpers,2.1.0,MIT
grape_logging,1.7.0,MIT
-grpc,1.6.0,Apache 2.0
+grpc,1.6.6,Apache 2.0
gzip-size,3.0.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
handlebars,4.0.6,MIT
+har-schema,1.0.5,ISC
+har-validator,4.2.1,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary,0.1.7,MIT
has-cors,1.1.0,MIT
+has-flag,1.0.0,MIT
has-flag,2.0.0,MIT
has-symbol-support-x,1.3.0,MIT
has-to-string-tag-x,1.3.0,MIT
+has-unicode,2.0.1,ISC
hash-sum,1.0.2,MIT
hash.js,1.0.3,MIT
hashie,3.5.6,MIT
hashie-forbidden_attributes,0.1.1,MIT
+hawk,3.1.3,New BSD
he,1.1.1,MIT
health_check,2.6.0,MIT
hipchat,1.5.2,MIT
+hoek,2.16.3,New BSD
home-or-tmp,2.0.0,MIT
hosted-git-info,2.2.0,ISC
hpack.js,2.1.6,MIT
@@ -489,10 +570,12 @@ htmlparser2,3.9.2,MIT
http,0.9.8,MIT
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
+http-errors,1.6.1,MIT
http-errors,1.6.2,MIT
http-form_data,1.0.1,MIT
http-proxy,1.16.2,MIT
http-proxy-middleware,0.17.4,MIT
+http-signature,1.1.1,MIT
http_parser.rb,0.6.0,MIT
httparty,0.13.7,MIT
httpclient,2.8.2,ruby
@@ -513,6 +596,7 @@ indexof,0.0.1,unknown
infinity-agent,2.0.3,MIT
inflight,1.0.6,ISC
influxdb,0.2.3,MIT
+inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.4,ISC
inquirer,0.12.0,MIT
@@ -532,12 +616,16 @@ is-builtin-module,1.0.0,MIT
is-dotfile,1.0.2,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
is-extglob,2.1.1,MIT
is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
is-fullwidth-code-point,2.0.0,MIT
+is-glob,2.0.1,MIT
is-glob,3.1.0,MIT
is-my-json-valid,2.16.0,MIT
is-npm,1.0.0,MIT
+is-number,0.1.1,MIT
is-number,2.1.0,MIT
is-object,1.0.1,MIT
is-path-cwd,1.0.0,MIT
@@ -553,13 +641,16 @@ is-resolvable,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
is-stream,1.1.0,MIT
is-svg,2.1.0,MIT
+is-typedarray,1.0.0,MIT
is-unc-path,0.1.2,MIT
is-utf8,0.2.1,MIT
is-windows,0.2.0,MIT
+isarray,0.0.1,MIT
isarray,1.0.0,MIT
isbinaryfile,3.0.2,MIT
-isexe,2.0.0,ISC
+isexe,1.1.2,ISC
isobject,2.1.0,MIT
+isstream,0.1.2,MIT
istanbul,0.4.5,New BSD
istanbul-api,1.1.1,New BSD
istanbul-lib-coverage,1.0.1,New BSD
@@ -573,6 +664,7 @@ jasmine-core,2.6.3,MIT
jasmine-jquery,2.1.1,MIT
jed,1.1.1,MIT
jira-ruby,1.4.1,MIT
+jodid25519,1.0.2,MIT
jquery,2.2.1,MIT
jquery-atwho-rails,1.3.2,MIT
jquery-rails,4.1.1,MIT
@@ -582,18 +674,23 @@ js-beautify,1.6.12,MIT
js-cookie,2.1.3,MIT
js-tokens,3.0.1,MIT
js-yaml,3.7.0,MIT
+js-yaml,3.9.1,MIT
+jsbn,0.1.1,MIT
+jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
json-jwt,1.7.2,MIT
json-loader,0.5.7,MIT
+json-schema,0.2.3,"AFLv2.1,BSD"
json-schema-traverse,0.3.1,MIT
json-stable-stringify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
-json3,3.3.2,[Circular]
+json3,3.3.2,MIT
json5,0.5.1,MIT
jsonfile,2.4.0,MIT
jsonify,0.0.0,Public Domain
jsonpointer,4.0.1,MIT
+jsprim,1.4.0,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
jszip-utils,0.0.2,MIT or GPLv3
jwt,1.5.6,MIT
@@ -619,11 +716,14 @@ levn,0.3.0,MIT
licensee,8.7.0,MIT
lie,3.1.1,MIT
little-plugger,1.1.4,MIT
+load-json-file,1.1.0,MIT
load-json-file,2.0.0,MIT
loader-runner,2.3.0,MIT
+loader-utils,0.2.16,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
+lodash,3.10.1,MIT
lodash,4.17.4,MIT
lodash._baseassign,3.2.0,MIT
lodash._basecopy,3.0.1,MIT
@@ -634,11 +734,13 @@ lodash._getnative,3.9.1,MIT
lodash._isiterateecall,3.0.9,MIT
lodash._topath,3.8.1,MIT
lodash.assign,3.2.0,MIT
+lodash.camelcase,4.1.1,MIT
lodash.camelcase,4.3.0,MIT
lodash.capitalize,4.2.1,MIT
lodash.cond,4.5.2,MIT
lodash.deburr,4.1.0,MIT
lodash.defaults,3.1.2,MIT
+lodash.get,3.7.0,MIT
lodash.get,4.4.2,MIT
lodash.isarguments,3.1.0,MIT
lodash.isarray,3.0.4,MIT
@@ -658,7 +760,9 @@ loofah,2.0.3,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
+lru-cache,2.2.4,MIT
lru-cache,3.2.0,ISC
+lru-cache,4.0.2,ISC
macaddress,0.2.8,MIT
mail,2.6.6,MIT
mail_room,0.9.1,MIT
@@ -670,6 +774,7 @@ math-expression-evaluator,1.2.16,MIT
media-typer,0.3.0,MIT
mem,1.1.0,MIT
memoist,0.16.0,MIT
+memory-fs,0.2.0,MIT
memory-fs,0.4.1,MIT
meow,3.7.0,MIT
merge-descriptors,1.0.1,MIT
@@ -677,8 +782,10 @@ method_source,0.8.2,MIT
methods,1.1.2,MIT
micromatch,2.3.11,MIT
miller-rabin,4.0.0,MIT
-mime,1.3.4,[Circular]
+mime,1.3.4,MIT
+mime-db,1.27.0,MIT
mime-db,1.29.0,MIT
+mime-types,2.1.15,MIT
mime-types,3.1,MIT
mime-types-data,3.2016.0521,MIT
mimemagic,0.3.0,MIT
@@ -687,13 +794,17 @@ mimic-response,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimatch,3.0.3,ISC
+minimatch,3.0.4,ISC
minimist,0.0.8,MIT
+minimist,1.2.0,MIT
mkdirp,0.5.1,MIT
mmap2,2.2.7,ruby
moment,2.17.1,MIT
-monaco-editor,0.8.3,MIT
+monaco-editor,0.10.0,MIT
mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.1,MIT
+ms,0.7.2,MIT
ms,2.0.0,MIT
multi_json,1.12.2,MIT
multi_xml,0.6.0,MIT
@@ -703,7 +814,9 @@ multipart-post,2.0.0,MIT
mustermann,1.0.0,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.5,ISC
+mysql2,0.4.5,MIT
name-all-modules-plugin,1.0.1,MIT
+nan,2.6.2,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
nested-error-stacks,1.0.2,MIT
@@ -712,21 +825,30 @@ net-ssh,4.1.0,MIT
netrc,0.11.0,MIT
node-dir,0.1.17,MIT
node-forge,0.6.33,BSD
+node-libs-browser,1.1.1,MIT
node-libs-browser,2.0.0,MIT
+node-pre-gyp,0.6.36,New BSD
+node-pre-gyp,0.6.37,New BSD
nodemon,1.11.0,MIT
nokogiri,1.8.1,MIT
+nopt,1.0.10,MIT
nopt,3.0.6,ISC
-normalize-package-data,2.3.5,Simplified BSD
+nopt,4.0.1,ISC
+normalize-package-data,2.4.0,Simplified BSD
normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
npm-run-path,2.0.2,MIT
+npmlog,4.1.0,ISC
null-check,1.0.0,MIT
num2fraction,1.2.2,MIT
number-is-nan,1.0.1,MIT
numerizer,0.1.1,MIT
oauth,0.5.1,MIT
+oauth-sign,0.8.2,Apache 2.0
oauth2,1.4.0,MIT
+object-assign,3.0.0,MIT
+object-assign,4.1.0,MIT
object-assign,4.1.1,MIT
object-component,0.0.3,unknown
object.omit,2.0.1,MIT
@@ -766,6 +888,7 @@ orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.4,ISC
@@ -776,6 +899,7 @@ p-locate,2.0.0,MIT
p-map,1.1.1,MIT
p-timeout,1.2.0,MIT
package-json,1.2.0,MIT
+pako,0.2.9,MIT
pako,1.0.5,(MIT AND Zlib)
paranoia,2.3.1,MIT
parse-asn1,5.0.0,ISC
@@ -786,28 +910,34 @@ parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
parseurl,1.3.1,MIT
path-browserify,0.0.0,MIT
+path-exists,2.1.0,MIT
path-exists,3.0.0,MIT
path-is-absolute,1.0.1,MIT
path-is-inside,1.0.2,(WTFPL OR MIT)
path-key,2.0.1,MIT
path-parse,1.0.5,MIT
path-to-regexp,0.1.7,MIT
+path-type,1.1.0,MIT
path-type,2.0.0,MIT
pause-stream,0.0.11,"MIT,Apache2"
pbkdf2,3.0.9,MIT
peek,1.0.1,MIT
peek-gc,0.0.2,MIT
peek-host,1.0.0,MIT
+peek-mysql2,1.1.0,MIT
peek-performance_bar,1.3.0,MIT
peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
peek-sidekiq,1.0.3,MIT
+performance-now,0.2.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
pify,2.3.0,MIT
-pikaday,1.5.1,"BSD,MIT"
+pify,3.0.0,MIT
+pikaday,1.6.1,MIT
pinkie,2.0.4,MIT
pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
pkg-dir,2.0.0,MIT
pkg-up,1.0.0,MIT
pluralize,1.2.1,MIT
@@ -860,7 +990,7 @@ private,0.1.7,MIT
process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
progress,1.1.8,MIT
-prometheus-client-mmap,0.7.0.beta14,Apache 2.0
+prometheus-client-mmap,0.7.0.beta18,Apache 2.0
proto-list,1.2.4,ISC
proxy-addr,1.1.5,MIT
prr,0.0.0,MIT
@@ -868,15 +998,18 @@ ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
public_suffix,3.0.0,MIT
+punycode,1.3.2,MIT
punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
q,1.5.0,MIT
qjobs,1.1.5,MIT
+qs,6.4.0,New BSD
qs,6.5.0,New BSD
query-string,4.3.2,MIT
querystring,0.2.0,MIT
-querystring-es3,0.2.1,[Circular]
+querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
+querystringify,1.0.0,MIT
rack,1.6.8,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
@@ -908,9 +1041,14 @@ rdoc,4.2.2,ruby
re2,1.1.1,New BSD
react-dev-utils,0.5.2,New BSD
read-all-stream,3.1.0,MIT
+read-pkg,1.1.0,MIT
read-pkg,2.0.0,MIT
+read-pkg-up,1.0.1,MIT
read-pkg-up,2.0.0,MIT
+readable-stream,1.0.34,MIT
readable-stream,2.0.6,MIT
+readable-stream,2.2.9,MIT
+readable-stream,2.3.3,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
@@ -932,21 +1070,26 @@ regenerate,1.3.2,MIT
regenerator-runtime,0.10.1,MIT
regenerator-transform,0.9.8,BSD
regex-cache,0.4.3,MIT
+regexpu-core,1.0.0,MIT
regexpu-core,2.0.0,MIT
registry-url,3.1.0,MIT
regjsgen,0.2.0,MIT
regjsparser,0.1.5,BSD
remove-trailing-separator,1.1.0,ISC
repeat-element,1.1.2,MIT
+repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
+repeating,1.1.3,MIT
repeating,2.0.1,MIT
representable,3.0.4,MIT
+request,2.81.0,Apache 2.0
request_store,1.3.1,MIT
require-directory,2.1.1,MIT
require-from-string,1.2.1,MIT
require-main-filename,1.0.1,ISC
require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
+resolve,1.1.7,MIT
resolve,1.2.0,MIT
resolve-from,1.0.1,MIT
responders,2.3.0,MIT
@@ -972,6 +1115,7 @@ rugged,0.26.0,MIT
run-async,0.1.0,MIT
rx-lite,3.1.2,Apache 2.0
safe-buffer,5.0.1,MIT
+safe-buffer,5.1.1,MIT
safe_yaml,1.0.4,MIT
sanitize,2.1.0,MIT
sass,3.4.22,MIT
@@ -985,6 +1129,7 @@ select-hose,2.0.0,MIT
select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
selfsigned,1.10.1,MIT
+semver,4.3.6,ISC
semver,5.3.0,ISC
semver-diff,2.1.0,MIT
send,0.15.4,MIT
@@ -1011,14 +1156,20 @@ slack-notifier,1.5.1,MIT
slash,1.0.0,MIT
slice-ansi,0.0.4,MIT
slide,1.1.6,ISC
+sntp,1.0.9,BSD
socket.io,1.7.3,MIT
socket.io-adapter,0.5.0,MIT
socket.io-client,1.7.3,MIT
socket.io-parser,2.3.1,MIT
sockjs,0.3.18,MIT
sockjs-client,1.0.1,MIT
+sockjs-client,1.1.4,MIT
sort-keys,1.1.2,MIT
+source-list-map,0.1.8,MIT
source-list-map,2.0.0,MIT
+source-map,0.1.43,BSD
+source-map,0.2.0,BSD
+source-map,0.4.4,New BSD
source-map,0.5.6,New BSD
source-map-support,0.4.11,MIT
spdx-correct,1.0.2,Apache 2.0
@@ -1031,6 +1182,7 @@ sprintf-js,1.0.3,New BSD
sprockets,3.7.1,MIT
sprockets-rails,3.2.0,MIT
sql.js,0.4.0,MIT
+sshpk,1.13.0,MIT
state_machines,0.4.0,MIT
state_machines-activemodel,0.4.0,MIT
state_machines-activerecord,0.4.0,MIT
@@ -1042,19 +1194,29 @@ stream-shift,1.0.0,MIT
strict-uri-encode,1.1.0,MIT
string-length,1.0.1,MIT
string-width,1.0.2,MIT
+string-width,2.0.0,MIT
string_decoder,0.10.31,MIT
+string_decoder,1.0.1,MIT
+string_decoder,1.0.3,MIT
stringex,2.7.1,MIT
+stringstream,0.0.5,MIT
strip-ansi,3.0.1,MIT
+strip-bom,2.0.0,MIT
strip-bom,3.0.0,MIT
strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
+supports-color,2.0.0,MIT
supports-color,3.2.3,MIT
+supports-color,4.2.1,MIT
svg4everybody,2.1.9,CC0-1.0
svgo,0.7.2,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
+tapable,0.1.10,MIT
tapable,0.2.8,MIT
+tar,2.2.1,ISC
+tar-pack,3.4.0,Simplified BSD
temple,0.7.7,MIT
test-exclude,4.0.0,ISC
text,1.3.1,MIT
@@ -1067,9 +1229,10 @@ three-stl-loader,1.0.4,MIT
through,2.3.8,MIT
thunky,0.1.0,unknown
tilt,2.0.6,MIT
-time-stamp,2.0.0,MIT
timeago.js,2.0.5,MIT
+timed-out,2.0.0,MIT
timed-out,4.0.1,MIT
+timers-browserify,1.4.2,MIT
timers-browserify,2.0.4,MIT
timfel-krb5-auth,0.8.3,LGPL
tiny-emitter,1.1.0,MIT
@@ -1079,15 +1242,20 @@ to-arraybuffer,1.0.1,MIT
to-fast-properties,1.0.2,MIT
toml-rb,0.3.15,MIT
touch,1.0.0,ISC
+tough-cookie,2.3.2,New BSD
traverse,0.6.6,MIT
trim-newlines,1.0.0,MIT
trim-right,1.0.1,MIT
truncato,0.7.10,MIT
tryit,1.0.3,MIT
+ts-loader,3.1.1,MIT
tty-browserify,0.0.0,MIT
+tunnel-agent,0.6.0,Apache 2.0
+tweetnacl,0.14.5,Unlicense
type-check,0.3.2,MIT
type-is,1.6.15,MIT
typedarray,0.0.6,MIT
+typescript,2.6.1,Apache 2.0
tzinfo,1.2.3,MIT
u2f,0.2.1,MIT
uber,0.1.0,MIT
@@ -1095,6 +1263,8 @@ uglifier,2.7.2,MIT
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
uglifyjs-webpack-plugin,0.4.6,MIT
+uid-number,0.0.6,ISC
+ultron,1.0.2,MIT
ultron,1.1.0,MIT
unc-path-regex,0.1.2,MIT
undefsafe,0.0.3,MIT / http://rem.mit-license.org
@@ -1111,6 +1281,8 @@ update-notifier,0.5.0,Simplified BSD
url,0.11.0,MIT
url-loader,0.5.8,MIT
url-parse,1.0.5,MIT
+url-parse,1.1.7,MIT
+url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
@@ -1118,26 +1290,28 @@ user-home,2.0.0,MIT
useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
-utils-merge,1.0.0,[Circular]
+utils-merge,1.0.0,MIT
uuid,2.0.3,MIT
+uuid,3.0.1,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
vary,1.1.1,MIT
vendors,1.0.1,MIT
+verror,1.3.6,MIT
version_sorter,2.1.0,MIT
virtus,1.0.5,MIT
visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.2.6,MIT
+vue,2.5.2,MIT
vue-hot-reload-api,2.0.11,MIT
vue-loader,11.3.4,MIT
vue-resource,1.3.4,MIT
vue-style-loader,2.0.5,MIT
-vue-template-compiler,2.2.6,MIT
+vue-template-compiler,2.5.2,MIT
vue-template-es2015-compiler,1.5.1,MIT
-vuex,2.3.1,MIT
+vuex,3.0.0,MIT
warden,1.2.6,MIT
watchpack,1.4.0,MIT
wbuf,1.7.2,MIT
@@ -1151,15 +1325,20 @@ webpack-stats-plugin,0.1.5,MIT
websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
whet.extend,0.9.9,MIT
-which,1.3.0,ISC
+which,1.2.12,ISC
+which-module,1.0.0,ISC
which-module,2.0.0,ISC
+wide-align,1.1.2,ISC
wikicloth,0.8.1,MIT
window-size,0.1.0,MIT
+wordwrap,0.0.2,MIT/X11
+wordwrap,0.0.3,MIT
wordwrap,1.0.0,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
write-file-atomic,1.3.4,ISC
+ws,1.1.2,MIT
ws,2.3.1,MIT
wtf-8,1.0.0,MIT
xdg-basedir,2.0.0,MIT
@@ -1168,6 +1347,9 @@ xmlhttprequest-ssl,1.5.3,MIT
xtend,4.0.1,MIT
y18n,3.2.1,ISC
yallist,2.1.2,ISC
+yargs,3.10.0,MIT
+yargs,6.6.0,MIT
yargs,8.0.2,MIT
+yargs-parser,4.2.1,ISC
yargs-parser,7.0.0,ISC
yeast,0.1.2,MIT
diff --git a/yarn.lock b/yarn.lock
index efae3892c31..bf92370d44f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -101,12 +101,6 @@ ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-ansi-styles@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
- dependencies:
- color-convert "^1.9.0"
-
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -223,12 +217,18 @@ async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@2.4.1, async@^2.1.2, async@^2.1.4:
+async@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
dependencies:
lodash "^4.14.0"
+async@^2.1.2, async@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -267,7 +267,7 @@ axios@^0.16.2:
follow-redirects "^1.2.3"
is-buffer "^1.1.5"
-babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+babel-code-frame@^6.11.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
dependencies:
@@ -275,6 +275,14 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
esutils "^2.0.2"
js-tokens "^3.0.0"
+babel-code-frame@^6.16.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
babel-core@^6.22.1, babel-core@^6.23.0:
version "6.23.1"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df"
@@ -924,7 +932,11 @@ bluebird@^2.10.2:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
-bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0:
+bluebird@^3.0.5, bluebird@^3.1.1:
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+
+bluebird@^3.3.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
@@ -1055,6 +1067,10 @@ buffer-indexof@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982"
+buffer-shims@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
+
buffer-xor@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@@ -1154,14 +1170,6 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
- dependencies:
- ansi-styles "^3.1.0"
- escape-string-regexp "^1.0.5"
- supports-color "^4.0.0"
-
chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -1245,7 +1253,7 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-color-convert@^1.3.0, color-convert@^1.9.0:
+color-convert@^1.3.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
dependencies:
@@ -1446,7 +1454,11 @@ copy-webpack-plugin@^4.0.1:
minimatch "^3.0.0"
node-dir "^0.1.10"
-core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
+core-js@^2.2.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
+
+core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -1985,7 +1997,7 @@ engine.io@1.8.3:
engine.io-parser "1.3.2"
ws "1.1.2"
-enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0:
+enhanced-resolve@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
dependencies:
@@ -2066,7 +2078,11 @@ es6-map@^0.1.3:
es6-symbol "~3.1.1"
event-emitter "~0.3.5"
-es6-promise@^3.0.2, es6-promise@~3.0.2:
+es6-promise@^3.0.2:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+
+es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
@@ -2247,6 +2263,10 @@ esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
+esprima@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
esprima@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
@@ -2647,11 +2667,11 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
mkdirp ">=0.5 0"
rimraf "2"
-function-bind@^1.0.2, function-bind@~1.1.0:
+function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
-function-bind@^1.1.1:
+function-bind@^1.1.1, function-bind@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2737,25 +2757,25 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+glob@^7.0.0, glob@^7.1.1, glob@~7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "^3.0.2"
+ minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@~7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+glob@^7.0.3, glob@^7.0.5:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "^3.0.4"
+ minimatch "^3.0.2"
once "^1.3.0"
path-is-absolute "^1.0.0"
@@ -2958,10 +2978,14 @@ html-comment-regex@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
-html-entities@1.2.0, html-entities@^1.2.0:
+html-entities@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+
htmlparser2@^3.8.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -3367,6 +3391,10 @@ isexe@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@@ -3510,7 +3538,18 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
-js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^3.1.1"
+
+js-yaml@^3.5.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
dependencies:
@@ -4051,7 +4090,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.29.0 < 2":
+"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
@@ -4059,7 +4098,13 @@ mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
-mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
+mime-types@^2.1.12, mime-types@~2.1.7:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
+ dependencies:
+ mime-db "~1.29.0"
+
+mime-types@~2.1.11, mime-types@~2.1.15:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
@@ -4283,7 +4328,16 @@ nopt@~1.0.10:
dependencies:
abbrev "1"
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-package-data@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
dependencies:
@@ -4458,7 +4512,7 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -5154,7 +5208,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.0, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9:
+readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
@@ -5177,6 +5231,18 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
+readable-stream@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
+ dependencies:
+ buffer-shims "^1.0.0"
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
readable-stream@~1.0.2:
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -5462,10 +5528,14 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+semver@^5.0.3:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -5873,7 +5943,7 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co
dependencies:
has-flag "^1.0.0"
-supports-color@^4.0.0, supports-color@^4.2.1:
+supports-color@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
dependencies:
@@ -5987,6 +6057,10 @@ thunky@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
+time-stamp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
+
timeago.js@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
@@ -6015,12 +6089,18 @@ tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
-tmp@0.0.31, tmp@0.0.x:
+tmp@0.0.31:
version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies:
os-tmpdir "~1.0.1"
+tmp@0.0.x:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@@ -6061,15 +6141,6 @@ tryit@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
-ts-loader@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.1.1.tgz#602d93c12029eaf8fa1ee478a90785d40c5f6658"
- dependencies:
- chalk "^2.3.0"
- enhanced-resolve "^3.0.0"
- loader-utils "^1.0.2"
- semver "^5.0.1"
-
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -6101,10 +6172,6 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"
-
uglify-js@^2.6, uglify-js@^2.8.29:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
@@ -6380,7 +6447,7 @@ webpack-bundle-analyzer@^2.8.2:
opener "^1.4.3"
ws "^2.3.1"
-webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
+webpack-dev-middleware@^1.0.11:
version "1.11.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9"
dependencies:
@@ -6389,6 +6456,16 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
+webpack-dev-middleware@^1.11.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709"
+ dependencies:
+ memory-fs "~0.4.1"
+ mime "^1.3.4"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ time-stamp "^2.0.0"
+
webpack-dev-server@^2.6.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8"
@@ -6477,12 +6554,18 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1, which@^1.2.9:
+which@^1.1.1, which@^1.2.1:
version "1.2.12"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
dependencies:
isexe "^1.1.1"
+which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
wide-align@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"