summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml38
-rw-r--r--.gitlab-ci.yml21
-rw-r--r--.scss-lint.yml68
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/notes.js225
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue58
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js18
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss1
-rw-r--r--app/assets/stylesheets/framework/notes.scss14
-rw-r--r--app/assets/stylesheets/framework/selects.scss1
-rw-r--r--app/assets/stylesheets/framework/timeline.scss34
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss1
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss100
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb3
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/helpers/projects_helper.rb34
-rw-r--r--app/helpers/submodule_helper.rb1
-rw-r--r--app/models/ci/pipeline_schedule.rb10
-rw-r--r--app/models/concerns/routable.rb83
-rw-r--r--app/models/concerns/select_for_project_authorization.rb6
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb24
-rw-r--r--app/models/project_authorization.rb6
-rw-r--r--app/models/project_services/jira_service.rb37
-rw-r--r--app/models/user.rb38
-rw-r--r--app/serializers/merge_request_entity.rb1
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/issues/reopen_service.rb1
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/reopen_service.rb1
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb40
-rw-r--r--app/views/admin/system_info/show.html.haml5
-rw-r--r--app/views/dashboard/_activities.html.haml3
-rw-r--r--app/views/dashboard/activity.html.haml12
-rw-r--r--app/views/dashboard/projects/index.html.haml22
-rw-r--r--app/views/dashboard/projects/starred.html.haml18
-rw-r--r--app/views/discussions/_notes.html.haml14
-rw-r--r--app/views/events/_event_last_push.html.haml14
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml3
-rw-r--r--app/views/groups/_head.html.haml3
-rw-r--r--app/views/groups/_show_nav.html.haml7
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml34
-rw-r--r--app/views/projects/activity.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml5
-rw-r--r--app/views/projects/merge_requests/index.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml3
-rw-r--r--app/views/projects/pipelines/charts/_overall.haml6
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/search/_category.html.haml77
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml10
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml23
-rw-r--r--changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml4
-rw-r--r--changelogs/unreleased/27439-memory-usage-info.yml4
-rw-r--r--changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml4
-rw-r--r--changelogs/unreleased/31448-jira-urls.yml4
-rw-r--r--changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml4
-rw-r--r--changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml4
-rw-r--r--changelogs/unreleased/32851-postgres-min-version.yml4
-rw-r--r--changelogs/unreleased/ci-build-pipeline-header-vue.yml4
-rw-r--r--changelogs/unreleased/dm-consistent-last-push-event.yml4
-rw-r--r--changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml4
-rw-r--r--changelogs/unreleased/issuable-form-create-label-sub-groups.yml4
-rw-r--r--changelogs/unreleased/rework-authorizations-performance.yml6
-rw-r--r--changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml4
-rw-r--r--config/initializers/0_acts_as_taggable.rb (renamed from config/initializers/acts_as_taggable.rb)4
-rw-r--r--config/initializers/postgresql_cte.rb132
-rw-r--r--config/initializers/server_uptime.rb1
-rw-r--r--config/webpack.config.js20
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb1
-rw-r--r--db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb1
-rw-r--r--db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb1
-rw-r--r--db/migrate/20160919144305_add_type_to_labels.rb1
-rw-r--r--db/migrate/20161018124658_make_project_owners_masters.rb1
-rw-r--r--db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb1
-rw-r--r--db/migrate/20170320173259_migrate_assignees.rb4
-rw-r--r--db/migrate/20170503140201_reschedule_project_authorizations.rb44
-rw-r--r--db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb123
-rw-r--r--db/migrate/20170504182103_add_index_project_group_links_group_id.rb19
-rw-r--r--db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb4
-rw-r--r--db/post_migrate/20170309171644_reset_relative_position_for_issue.rb4
-rw-r--r--db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb1
-rw-r--r--db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb15
-rw-r--r--db/schema.rb3
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md3
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/user/group/subgroups/index.md9
-rw-r--r--doc/user/project/integrations/jira.md3
-rw-r--r--features/dashboard/starred_projects.feature12
-rw-r--r--features/project/merge_requests/accept.feature3
-rw-r--r--features/steps/dashboard/dashboard.rb2
-rw-r--r--features/steps/project/merge_requests/acceptance.rb6
-rw-r--r--features/steps/project/services.rb6
-rw-r--r--lib/api/entities.rb5
-rw-r--r--lib/api/groups.rb6
-rw-r--r--lib/api/services.rb8
-rw-r--r--lib/api/v3/entities.rb5
-rw-r--r--lib/api/v3/groups.rb6
-rw-r--r--lib/gitlab/ci/trace/stream.rb51
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb6
-rw-r--r--lib/gitlab/dependency_linker/json_linker.rb4
-rw-r--r--lib/gitlab/git/diff_collection.rb39
-rw-r--r--lib/gitlab/group_hierarchy.rb104
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb17
-rw-r--r--lib/gitlab/project_authorizations/with_nested_groups.rb125
-rw-r--r--lib/gitlab/project_authorizations/without_nested_groups.rb35
-rw-r--r--lib/gitlab/sql/recursive_cte.rb62
-rw-r--r--lib/support/init.d/gitlab.default.example2
-rw-r--r--package.json5
-rw-r--r--qa/Dockerfile23
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock10
-rw-r--r--qa/qa/specs/config.rb35
-rw-r--r--qa/spec/spec_helper.rb1
-rw-r--r--rubocop/cop/migration/update_column_in_batches.rb43
-rw-r--r--rubocop/migration_helpers.rb5
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/trigger-build3
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb22
-rw-r--r--spec/controllers/groups_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb7
-rw-r--r--spec/factories/projects.rb12
-rw-r--r--spec/features/admin/admin_system_info_spec.rb3
-rw-r--r--spec/features/dashboard/projects_spec.rb14
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb4
-rw-r--r--spec/features/groups/members/list_spec.rb4
-rw-r--r--spec/features/groups_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb31
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb13
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb10
-rw-r--r--spec/features/merge_requests/widget_spec.rb21
-rw-r--r--spec/features/projects/group_links_spec.rb2
-rw-r--r--spec/features/projects/members/sorting_spec.rb11
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb3
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb24
-rw-r--r--spec/finders/group_members_finder_spec.rb2
-rw-r--r--spec/finders/members_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json3
-rw-r--r--spec/helpers/submodule_helper_spec.rb5
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js9
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js45
-rw-r--r--spec/javascripts/notes_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js82
-rw-r--r--spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js68
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb43
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb11
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb40
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb53
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb83
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb8
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb73
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/sql/recursive_cte_spec.rb49
-rw-r--r--spec/migrations/fill_authorized_projects_spec.rb18
-rw-r--r--spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb66
-rw-r--r--spec/migrations/update_retried_for_ci_build_spec.rb (renamed from spec/migrations/update_retried_for_ci_builds_spec.rb)0
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb8
-rw-r--r--spec/models/concerns/routable_spec.rb117
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/members/project_member_spec.rb13
-rw-r--r--spec/models/milestone_spec.rb13
-rw-r--r--spec/models/namespace_spec.rb16
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb152
-rw-r--r--spec/models/project_spec.rb25
-rw-r--r--spec/models/project_team_spec.rb29
-rw-r--r--spec/models/user_spec.rb131
-rw-r--r--spec/policies/group_policy_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb26
-rw-r--r--spec/requests/api/projects_spec.rb20
-rw-r--r--spec/requests/api/v3/commits_spec.rb1
-rw-r--r--spec/requests/api/v3/groups_spec.rb2
-rw-r--r--spec/requests/api/v3/projects_spec.rb13
-rw-r--r--spec/rubocop/cop/migration/update_column_in_batches_spec.rb94
-rw-r--r--spec/services/issues/close_service_spec.rb6
-rw-r--r--spec/services/issues/reopen_service_spec.rb7
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb15
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/projects/destroy_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb9
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb66
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/support/issuable_shared_examples.rb7
-rw-r--r--yarn.lock218
223 files changed, 3271 insertions, 1174 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 00000000000..e5636a13783
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,38 @@
+---
+engines:
+ brakeman:
+ enabled: true
+ bundler-audit:
+ enabled: true
+ duplication:
+ enabled: true
+ config:
+ languages:
+ - ruby
+ - javascript
+ eslint:
+ enabled: true
+ fixme:
+ enabled: true
+ rubocop:
+ enabled: true
+ratings:
+ paths:
+ - Gemfile.lock
+ - "**.erb"
+ - "**.haml"
+ - "**.rb"
+ - "**.rhtml"
+ - "**.slim"
+ - "**.inc"
+ - "**.js"
+ - "**.jsx"
+ - "**.module"
+exclude_paths:
+- config/
+- db/
+- features/
+- node_modules/
+- spec/
+- vendor/
+- lib/api/v3/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0123b9b214f..dea11bb9f61 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,7 +52,7 @@ stages:
.use-pg: &use-pg
services:
- - postgres:9.1
+ - postgres:9.2
- redis:alpine
.use-mysql: &use-mysql
@@ -487,25 +487,6 @@ lint:javascript:report:
paths:
- eslint-report.html
-# Trigger docs build
-# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
-trigger_docs:
- stage: post-test
- image: "alpine"
- <<: *dedicated-runner
- before_script:
- - apk update && apk add curl
- variables:
- GIT_STRATEGY: "none"
- cache: {}
- artifacts: {}
- script:
- - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
- - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
-
pages:
before_script: []
stage: pages
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 83c68309fa8..db234ad739c 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -11,11 +11,11 @@ linters:
# !global, !important, and !optional flags.
BangFormat:
enabled: false
-
+
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
-
+
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
ChainedClasses:
@@ -25,13 +25,13 @@ linters:
# (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
-
+
# Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
enabled: true
-
+
# Which form of comments to prefer in CSS.
Comment:
enabled: false
@@ -39,7 +39,7 @@ linters:
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
-
+
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
@@ -54,19 +54,19 @@ linters:
# more information.
DisableLinterReason:
enabled: true
-
+
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
- enabled: false
-
+ enabled: true
+
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
-
+
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
-
+
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
@@ -75,49 +75,49 @@ linters:
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
- enabled: false
-
+ enabled: true
+
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
-
+
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
-
+
# Avoid using ID selectors.
IdSelector:
enabled: false
-
+
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
-
+
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
-
+
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
-
+
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
-
+
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
-
+
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
-
+
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
@@ -129,12 +129,12 @@ linters:
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
-
+
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
-
+
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
@@ -144,25 +144,25 @@ linters:
# be declared with one colon.
PseudoElement:
enabled: true
-
+
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
-
+
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
-
+
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
-
+
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
-
+
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
@@ -173,11 +173,11 @@ linters:
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
-
+
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
-
+
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
@@ -197,12 +197,12 @@ linters:
# colon.
SpaceAfterVariableName:
enabled: false
-
+
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
-
+
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
@@ -210,7 +210,7 @@ linters:
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
-
+
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
@@ -241,7 +241,7 @@ linters:
# be unnecessary.
UnnecessaryParentReference:
enabled: false
-
+
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index f1b07408671..57394097944 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) {
export function bytesToKiB(number) {
return number / BYTES_IN_KIB;
}
+
+/**
+ * Utility function that calculates MiB of the given bytes.
+ *
+ * @param {Number} number bytes
+ * @return {Number} MiB
+ */
+export function bytesToMiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB);
+}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 22032d0f914..894ed81b044 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
- if (anchor) {
+ if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b0b1cfd6c8a..0ca7cabfc5a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,10 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren,
+no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase,
+no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line,
+default-case, prefer-template, consistent-return, no-alert, no-return-assign,
+no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
+brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
+newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global Autosave */
/* global ResolveService */
@@ -57,7 +63,7 @@ const normalizeNewlines = function(str) {
this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
this.noteable_url = document.URL;
- this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
+ this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.flashErrors = [];
@@ -87,61 +93,61 @@ const normalizeNewlines = function(str) {
Notes.prototype.addBinding = function() {
// Edit note link
- $(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
- $(document).on("click", ".note-edit-cancel", this.cancelEdit);
+ $(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
+ $(document).on('click', '.note-edit-cancel', this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
- $(document).on("click", ".js-comment-submit-button", this.postComment);
- $(document).on("click", ".js-comment-save-button", this.updateComment);
- $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ $(document).on('click', '.js-comment-submit-button', this.postComment);
+ $(document).on('click', '.js-comment-save-button', this.updateComment);
+ $(document).on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
$(document).on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
- $(document).on("click", ".js-note-delete", this.removeNote);
+ $(document).on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+ $(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
- $(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+ $(document).on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+ $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote);
+ $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
- $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote);
+ $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// hide diff note form
- $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
+ $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
- $(document).on("visibilitychange", this.visibilityChange);
+ $(document).on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
- $(document).on("issuable:change", this.refresh);
+ $(document).on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
- $(document).on("ajax:success", ".js-main-target-form", this.addNote);
- $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
- $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
- $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
+ $(document).on('ajax:success', '.js-main-target-form', this.addNote);
+ $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
+ $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// when a key is clicked on the notes
- return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
+ return $(document).on('keydown', '.js-note-text', this.keydownNoteText);
};
Notes.prototype.cleanBinding = function() {
- $(document).off("click", ".js-note-edit");
- $(document).off("click", ".note-edit-cancel");
- $(document).off("click", ".js-note-delete");
- $(document).off("click", ".js-note-attachment-delete");
- $(document).off("click", ".js-discussion-reply-button");
- $(document).off("click", ".js-add-diff-note-button");
- $(document).off("visibilitychange");
- $(document).off("keyup input", ".js-note-text");
- $(document).off("click", ".js-note-target-reopen");
- $(document).off("click", ".js-note-target-close");
- $(document).off("click", ".js-note-discard");
- $(document).off("keydown", ".js-note-text");
+ $(document).off('click', '.js-note-edit');
+ $(document).off('click', '.note-edit-cancel');
+ $(document).off('click', '.js-note-delete');
+ $(document).off('click', '.js-note-attachment-delete');
+ $(document).off('click', '.js-discussion-reply-button');
+ $(document).off('click', '.js-add-diff-note-button');
+ $(document).off('visibilitychange');
+ $(document).off('keyup input', '.js-note-text');
+ $(document).off('click', '.js-note-target-reopen');
+ $(document).off('click', '.js-note-target-close');
+ $(document).off('click', '.js-note-discard');
+ $(document).off('keydown', '.js-note-text');
$(document).off('click', '.js-comment-resolve-button');
- $(document).off("click", '.system-note-commit-list-toggler');
- $(document).off("ajax:success", ".js-main-target-form");
- $(document).off("ajax:success", ".js-discussion-note-form");
- $(document).off("ajax:complete", ".js-main-target-form");
+ $(document).off('click', '.system-note-commit-list-toggler');
+ $(document).off('ajax:success', '.js-main-target-form');
+ $(document).off('ajax:success', '.js-discussion-note-form');
+ $(document).off('ajax:complete', '.js-main-target-form');
};
Notes.initCommentTypeToggle = function (form) {
@@ -231,8 +237,8 @@ const normalizeNewlines = function(str) {
this.refreshing = true;
return $.ajax({
url: this.notes_url,
- headers: { "X-Last-Fetched-At": this.last_fetched_at },
- dataType: "json",
+ headers: { 'X-Last-Fetched-At': this.last_fetched_at },
+ dataType: 'json',
success: (function(_this) {
return function(data) {
var notes;
@@ -303,7 +309,7 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) {
- if (noteEntity.discussion_html != null) {
+ if (noteEntity.discussion_html) {
return this.renderDiscussionNote(noteEntity, $form);
}
@@ -368,8 +374,8 @@ const normalizeNewlines = function(str) {
return;
}
this.note_ids.push(noteEntity.id);
- form = $form || $(".js-discussion-note-form[data-discussion-id='" + noteEntity.discussion_id + "']");
- row = form.closest("tr");
+ form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
+ row = form.closest('tr');
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
@@ -386,7 +392,7 @@ const normalizeNewlines = function(str) {
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find('.notes[data-discussion-id="' + noteEntity.discussion_id + '"]');
+ var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
var contentContainerClass = '.' + $notes.closest('.notes_content')
.attr('class')
.split(' ')
@@ -397,7 +403,7 @@ const normalizeNewlines = function(str) {
}
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
- if ((page && page.indexOf('projects:merge_request') === 0) || !noteEntity.diff_discussion_html) {
+ if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
}
} else {
@@ -450,13 +456,13 @@ const normalizeNewlines = function(str) {
Notes.prototype.resetMainTargetForm = function(e) {
var form;
- form = $(".js-main-target-form");
+ form = $('.js-main-target-form');
// remove validation errors
- form.find(".js-errors").remove();
+ form.find('.js-errors').remove();
// reset text and preview
- form.find(".js-md-write-button").click();
- form.find(".js-note-text").val("").trigger("input");
- form.find(".js-note-text").data("autosave").reset();
+ form.find('.js-md-write-button').click();
+ form.find('.js-note-text').val('').trigger('input');
+ form.find('.js-note-text').data('autosave').reset();
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
@@ -467,8 +473,8 @@ const normalizeNewlines = function(str) {
Notes.prototype.reenableTargetFormSubmitButton = function() {
var form;
- form = $(".js-main-target-form");
- return form.find(".js-note-text").trigger("input");
+ form = $('.js-main-target-form');
+ return form.find('.js-note-text').trigger('input');
};
/*
@@ -480,18 +486,18 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupMainTargetNoteForm = function() {
var form;
// find the form
- form = $(".js-new-note-form");
+ form = $('.js-new-note-form');
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
this.setupNoteForm(form);
// fix classes
- form.removeClass("js-new-note-form");
- form.addClass("js-main-target-form");
- form.find("#note_line_code").remove();
- form.find("#note_position").remove();
- form.find("#note_type").val('');
- form.find("#in_reply_to_discussion_id").remove();
+ form.removeClass('js-new-note-form');
+ form.addClass('js-main-target-form');
+ form.find('#note_line_code').remove();
+ form.find('#note_position').remove();
+ form.find('#note_type').val('');
+ form.find('#in_reply_to_discussion_id').remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
this.parentTimeline = form.parents('.timeline');
@@ -512,20 +518,20 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNoteForm = function(form) {
var textarea, key;
new gl.GLForm(form, this.enableGFM);
- textarea = form.find(".js-note-text");
+ textarea = form.find('.js-note-text');
key = [
- "Note",
- form.find("#note_noteable_type").val(),
- form.find("#note_noteable_id").val(),
- form.find("#note_commit_id").val(),
- form.find("#note_type").val(),
- form.find("#in_reply_to_discussion_id").val(),
+ 'Note',
+ form.find('#note_noteable_type').val(),
+ form.find('#note_noteable_id').val(),
+ form.find('#note_commit_id').val(),
+ form.find('#note_type').val(),
+ form.find('#in_reply_to_discussion_id').val(),
// LegacyDiffNote
- form.find("#note_line_code").val(),
+ form.find('#note_line_code').val(),
// DiffNote
- form.find("#note_position").val()
+ form.find('#note_position').val()
];
return new Autosave(textarea, key);
};
@@ -670,7 +676,8 @@ const normalizeNewlines = function(str) {
const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
$note.replaceWith($newNote);
this.setupNewNote($newNote);
- this.updatedNotesTrackingMap[noteId] = null;
+ // Now that we have taken care of the update, clear it out
+ delete this.updatedNotesTrackingMap[noteId];
}
else {
$note.find('.js-finish-edit-warning').hide();
@@ -722,14 +729,14 @@ const normalizeNewlines = function(str) {
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
- $(".note[id='" + noteElId + "']").each((function(_this) {
+ $(`.note[id="${noteElId}"]`).each((function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
- // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
- // where $("#noteId") would return only one.
+ // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
+ // where $('#noteId') would return only one.
return function(i, el) {
var $note, $notes;
$note = $(el);
- $notes = $note.closest(".discussion-notes");
+ $notes = $note.closest('.discussion-notes');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
@@ -740,11 +747,11 @@ const normalizeNewlines = function(str) {
$note.remove();
// check if this is the last note for this line
- if ($notes.find(".note").length === 0) {
- var notesTr = $notes.closest("tr");
+ if ($notes.find('.note').length === 0) {
+ var notesTr = $notes.closest('tr');
// "Discussions" tab
- $notes.closest(".timeline-entry").remove();
+ $notes.closest('.timeline-entry').remove();
// The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) {
@@ -768,11 +775,11 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.removeAttachment = function() {
- const $note = $(this).closest(".note");
- $note.find(".note-attachment").remove();
- $note.find(".note-body > .note-text").show();
- $note.find(".note-header").show();
- return $note.find(".current-note-edit-form").remove();
+ const $note = $(this).closest('.note');
+ $note.find('.note-attachment').remove();
+ $note.find('.note-body > .note-text').show();
+ $note.find('.note-header').show();
+ return $note.find('.current-note-edit-form').remove();
};
/*
@@ -788,7 +795,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.replyToDiscussionNote = function(target) {
var form, replyLink;
form = this.cleanForm(this.formClone.clone());
- replyLink = $(target).closest(".js-discussion-reply-button");
+ replyLink = $(target).closest('.js-discussion-reply-button');
// insert the form after the button
replyLink
.closest('.discussion-reply-holder')
@@ -808,26 +815,26 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
// setup note target
- var discussionID = dataHolder.data("discussionId");
+ var discussionID = dataHolder.data('discussionId');
if (discussionID) {
- form.attr("data-discussion-id", discussionID);
- form.find("#in_reply_to_discussion_id").val(discussionID);
+ form.attr('data-discussion-id', discussionID);
+ form.find('#in_reply_to_discussion_id').val(discussionID);
}
- form.attr("data-line-code", dataHolder.data("lineCode"));
- form.find("#line_type").val(dataHolder.data("lineType"));
+ form.attr('data-line-code', dataHolder.data('lineCode'));
+ form.find('#line_type').val(dataHolder.data('lineType'));
- form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
- form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
- form.find("#note_commit_id").val(dataHolder.data("commitId"));
- form.find("#note_type").val(dataHolder.data("noteType"));
+ form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
+ form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
+ form.find('#note_commit_id').val(dataHolder.data('commitId'));
+ form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote
- form.find("#note_line_code").val(dataHolder.data("lineCode"));
+ form.find('#note_line_code').val(dataHolder.data('lineCode'));
// DiffNote
- form.find("#note_position").val(dataHolder.attr("data-position"));
+ form.find('#note_position').val(dataHolder.attr('data-position'));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
form.find('.js-note-target-close').remove();
@@ -836,7 +843,7 @@ const normalizeNewlines = function(str) {
form
.removeClass('js-main-target-form')
- .addClass("discussion-form js-discussion-note-form");
+ .addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
@@ -845,7 +852,7 @@ const normalizeNewlines = function(str) {
gl.diffNotesCompileComponents();
}
- form.find(".js-note-text").focus();
+ form.find('.js-note-text').focus();
form
.find('.js-comment-resolve-button')
.attr('data-discussion-id', discussionID);
@@ -878,21 +885,21 @@ const normalizeNewlines = function(str) {
}) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
$link = $(target);
- row = $link.closest("tr");
+ row = $link.closest('tr');
const nextRow = row.next();
let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
- hasNotes = targetRow.is(".notes_holder");
+ hasNotes = nextRow.is('.notes_holder');
addForm = false;
let lineTypeSelector = '';
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
+ rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
@@ -902,12 +909,12 @@ const normalizeNewlines = function(str) {
notesContent = targetRow.find(notesContentSelector);
if (notesContent.length) {
notesContent.show();
- replyButton = notesContent.find(".js-discussion-reply-button:visible");
+ replyButton = notesContent.find('.js-discussion-reply-button:visible');
if (replyButton.length) {
this.replyToDiscussionNote(replyButton[0]);
} else {
// In parallel view, the form may not be present in one of the panes
- noteForm = notesContent.find(".js-discussion-note-form");
+ noteForm = notesContent.find('.js-discussion-note-form');
if (noteForm.length === 0) {
addForm = true;
}
@@ -945,15 +952,15 @@ const normalizeNewlines = function(str) {
Notes.prototype.removeDiscussionNoteForm = function(form) {
var glForm, row;
- row = form.closest("tr");
+ row = form.closest('tr');
glForm = form.data('gl-form');
glForm.destroy();
- form.find(".js-note-text").data("autosave").reset();
+ form.find('.js-note-text').data('autosave').reset();
// show the reply button (will only work for replies)
form
.prev('.discussion-reply-holder')
.show();
- if (row.is(".js-temp-notes-holder")) {
+ if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
} else {
@@ -965,7 +972,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.cancelDiscussionForm = function(e) {
var form;
e.preventDefault();
- form = $(e.target).closest(".js-discussion-note-form");
+ form = $(e.target).closest('.js-discussion-note-form');
return this.removeDiscussionNoteForm(form);
};
@@ -977,10 +984,10 @@ const normalizeNewlines = function(str) {
Notes.prototype.updateFormAttachment = function() {
var filename, form;
- form = $(this).closest("form");
+ form = $(this).closest('form');
// get only the basename
- filename = $(this).val().replace(/^.*[\\\/]/, "");
- return form.find(".js-attachment-filename").text(filename);
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-attachment-filename').text(filename);
};
/*
@@ -1212,7 +1219,7 @@ const normalizeNewlines = function(str) {
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
- <a href="/${currentUsername}"><span class="dummy-avatar"></span></a>
+ <a href="/${currentUsername}"><span class="avatar dummy-avatar"></span></a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 486b13e60af..8155218681c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -1,4 +1,6 @@
import statusCodes from '~/lib/utils/http_status';
+import { bytesToMiB } from '~/lib/utils/number_utils';
+
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
@@ -9,8 +11,8 @@ export default {
},
data() {
return {
- // memoryFrom: 0,
- // memoryTo: 0,
+ memoryFrom: 0,
+ memoryTo: 0,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
+ memoryChangeType() {
+ const memoryTo = Number(this.memoryTo);
+ const memoryFrom = Number(this.memoryFrom);
+
+ if (memoryTo > memoryFrom) {
+ return 'increased';
+ } else if (memoryTo < memoryFrom) {
+ return 'decreased';
+ }
+
+ return 'unchanged';
+ },
},
methods: {
+ getMegabytes(bytesString) {
+ const valueInBytes = Number(bytesString).toFixed(2);
+ return (bytesToMiB(valueInBytes)).toFixed(2);
+ },
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
- const { memory_values } = metrics;
- // if (memory_previous.length > 0) {
- // this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
- // }
- //
- // if (memory_current.length > 0) {
- // this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
- // }
+ const { memory_before, memory_after, memory_values } = metrics;
+
+ // Both `memory_before` and `memory_after` objects
+ // have peculiar structure where accessing only a specific
+ // index yeilds correct value that we can use to show memory delta.
+ if (memory_before.length > 0) {
+ this.memoryFrom = this.getMegabytes(memory_before[0].value[1]);
+ }
+
+ if (memory_after.length > 0) {
+ this.memoryTo = this.getMegabytes(memory_after[0].value[1]);
+ }
if (memory_values.length > 0) {
this.hasMetrics = true;
@@ -102,7 +124,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- Deployment memory usage:
+ Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index d866d4e94b0..fcd4fdaf09f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -13,7 +13,7 @@ export default {
},
data() {
return {
- removeSourceBranch: true,
+ removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest
|| this.mr.preventMerge);
},
+ isRemoveSourceBranchButtonDisabled() {
+ return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
+ },
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
+ id="remove-source-branch-input"
v-model="removeSourceBranch"
- :disabled="isMergeButtonDisabled"
+ :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index c07bd25e6fd..dc4b2195050 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -50,7 +50,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
- this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
+ this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
new file mode 100644
index 00000000000..fd0dcd716d6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -0,0 +1,122 @@
+<script>
+import ciIconBadge from './ci_badge_link.vue';
+import timeagoTooltip from './time_ago_tooltip.vue';
+import tooltipMixin from '../mixins/tooltip';
+import userAvatarLink from './user_avatar/user_avatar_link.vue';
+
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ components: {
+ ciIconBadge,
+ timeagoTooltip,
+ userAvatarLink,
+ },
+
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
+ },
+ },
+
+ methods: {
+ onClickAction(action) {
+ this.$emit('postAction', action);
+ },
+ },
+};
+</script>
+<template>
+ <header class="page-content-header top-area">
+ <section class="header-main-content">
+
+ <ci-icon-badge :status="status" />
+
+ <strong>
+ {{itemName}} #{{itemId}}
+ </strong>
+
+ triggered
+
+ <timeago-tooltip :time="time" />
+
+ by
+
+ <user-avatar-link
+ :link-href="user.web_url"
+ :img-src="user.avatar_url"
+ :img-alt="userAvatarAltText"
+ :tooltip-text="user.name"
+ :img-size="24"
+ />
+
+ <a
+ :href="user.web_url"
+ :title="user.email"
+ class="js-user-link commit-committer-link"
+ ref="tooltip">
+ {{user.name}}
+ </a>
+ </section>
+
+ <section
+ class="header-action-button nav-controls"
+ v-if="actions.length">
+ <template
+ v-for="action in actions">
+ <a
+ v-if="action.type === 'link'"
+ :href="action.path"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
+ <button
+ v-else="action.type === 'button'"
+ @click="onClickAction(action)"
+ :class="action.cssClass"
+ type="button">
+ {{action.label}}
+ </button>
+
+ </template>
+ </section>
+ </header>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
new file mode 100644
index 00000000000..af2b4c6786e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -0,0 +1,58 @@
+<script>
+import tooltipMixin from '../mixins/tooltip';
+import timeagoMixin from '../mixins/timeago';
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Port of ruby helper time_ago_with_tooltip
+ */
+
+export default {
+ props: {
+ time: {
+ type: String,
+ required: true,
+ },
+
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+
+ shortFormat: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ timeagoMixin,
+ ],
+
+ computed: {
+ timeagoCssClass() {
+ return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
+ },
+ },
+};
+</script>
+<template>
+ <time
+ :class="[timeagoCssClass, cssClass]"
+ class="js-timeago js-timeago-render"
+ :title="tooltipTitle(time)"
+ :data-placement="tooltipPlacement"
+ data-container="body"
+ ref="tooltip">
+ {{timeFormated(time)}}
+ </time>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
new file mode 100644
index 00000000000..20f63ab663c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -0,0 +1,18 @@
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Mixin with time ago methods used in some vue components
+ */
+export default {
+ methods: {
+ timeFormated(time) {
+ const timeago = gl.utils.getTimeago();
+
+ return timeago.format(time);
+ },
+
+ tooltipTitle(time) {
+ return gl.utils.formatDate(time);
+ },
+ },
+};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d2ec1791d2b..b8ba77f4513 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -34,6 +34,7 @@
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@import "framework/tables.scss";
+@import "framework/notes.scss";
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index d64b1237b2c..75907c35b7e 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -10,7 +10,7 @@
top: 0;
margin-top: 3px;
padding: $gl-padding;
- z-index: 9;
+ z-index: 300;
width: 300px;
font-size: 14px;
background-color: $white-light;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 3dec911d289..fefe5575d9b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -23,7 +23,6 @@
.row-content-block {
margin-top: 0;
- margin-bottom: -$gl-padding;
background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index d86ae57cd9a..2d6bc17d4ff 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,5 +1,4 @@
gl-emoji {
- display: inline-block;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index eadb9409fee..25b4feca3c3 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -36,6 +36,10 @@
border-radius: 0;
}
}
+
+ &:empty {
+ margin: 0;
+ }
}
@media (max-width: $screen-sm-max) {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d76053fe72a..49163653548 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -11,7 +11,6 @@
> li {
padding: 10px 15px;
min-height: 20px;
- border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border;
&::after {
diff --git a/app/assets/stylesheets/framework/notes.scss b/app/assets/stylesheets/framework/notes.scss
new file mode 100644
index 00000000000..d349e3fad9c
--- /dev/null
+++ b/app/assets/stylesheets/framework/notes.scss
@@ -0,0 +1,14 @@
+@mixin notes-media($condition, $breakpoint-width) {
+ @media (#{$condition}-width: ($breakpoint-width)) {
+ @content;
+ }
+
+ // Diff is side by side
+ .notes_content.parallel & {
+ // We hide at double what we normally hide at because
+ // there are two columns of notes
+ @media (#{$condition}-width: (2 * $breakpoint-width)) {
+ @content;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 9ab17e67d4c..5ae833cd5f6 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -96,7 +96,6 @@
.select2-search-field input {
padding: 5px $gl-padding / 2;
- font-size: 13px;
height: auto;
font-family: inherit;
font-size: inherit;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index ddccfc96819..cec3b54d567 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -3,6 +3,12 @@
margin: 0;
padding: 0;
+ &::before {
+ @include notes-media('max', $screen-xs-max) {
+ background: none;
+ }
+ }
+
.system-note {
.note-text {
color: $gl-text-color !important;
@@ -23,6 +29,16 @@
.timeline-entry-inner {
position: relative;
+
+ @include notes-media('max', $screen-xs-max) {
+ .timeline-icon {
+ display: none;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
}
&:target,
@@ -40,24 +56,6 @@
}
}
-@media (max-width: $screen-xs-max) {
- .timeline {
- &::before {
- background: none;
- }
- }
-
- .timeline-entry .timeline-entry-inner {
- .timeline-icon {
- display: none;
- }
-
- .timeline-content {
- margin-left: 0;
- }
- }
-}
-
.discussion .timeline-entry {
margin: 0;
border-right: none;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 0c3407f34f8..785b09e622f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -21,6 +21,10 @@
margin-top: 0;
}
+ > :last-child {
+ margin-bottom: 0;
+ }
+
// Single code lines should wrap
code {
font-family: $monospace_font;
@@ -157,7 +161,7 @@
ul,
ol {
padding: 0;
- margin: 0 0 16px !important;
+ margin: 0 0 16px;
}
ul:dir(rtl),
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 68d7ab4bf84..ebe662136d5 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
+ // scss-lint:disable DuplicateProperty
height: calc(100vh - 222px);
+ // scss-lint:enable DuplicateProperty
min-height: 475px;
transition: width .2s;
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 90643832390..7b4eb689f1b 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -36,7 +36,6 @@
pre.commit-message {
background: none;
padding: 0;
- margin: 0;
border: none;
margin: 20px 0;
border-radius: 0;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 58715c4c083..b58922626fa 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -94,7 +94,6 @@
.old_line,
.new_line {
margin: 0;
- padding: 0;
border: none;
padding: 0 5px;
border-right: 1px solid;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 48d3b7b1d07..f269d53093d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -64,6 +64,10 @@
}
}
+ .btn .text-center {
+ display: inline;
+ }
+
.commit-title {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d79ae47f589..c2346f2f1c3 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -431,7 +431,7 @@
}
.detail-page-description {
- padding: 16px 0 0;
+ padding: 16px 0;
small {
color: $gray-darkest;
@@ -441,7 +441,7 @@
.edited-text {
color: $gray-darkest;
display: block;
- margin: 0 0 16px;
+ margin: 16px 0 0;
.author_link {
color: $gray-darkest;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index bee9b13b375..702e7662527 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle {
.fa-caret-down {
pointer-events: none;
- margin-left: 0;
color: inherit;
margin-left: 0;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 8dbac76e30a..971d54e7472 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -184,4 +184,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 49e453c7dbc..875e47cdff3 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
- margin: $gl-padding 0;
+ margin: $gl-padding 0 0;
}
.note-preview-holder {
@@ -124,10 +124,18 @@
}
.discussion-form {
- padding: $gl-padding-top $gl-padding;
+ padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
+.discussion-notes .disabled-comment {
+ padding: 6px 0;
+}
+
+.notes-form > li {
+ border: 0;
+}
+
.note-edit-form {
display: none;
font-size: 14px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a8a996c1035..cffd3b6060d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -14,24 +14,11 @@ ul.notes {
margin: 0;
padding: 0;
- .timeline-icon {
- float: left;
-
- svg {
- width: 16px;
- height: 16px;
- fill: $gray-darkest;
- position: absolute;
- left: 0;
- top: 16px;
- }
- }
-
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
- @media (max-width: $screen-sm-max) {
+ @include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
@@ -56,21 +43,22 @@ ul.notes {
position: relative;
}
- .note {
- padding: $gl-padding $gl-btn-padding 0;
+ > li {
+ padding: $gl-padding $gl-btn-padding;
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
+ &:last-child {
+ // Override `.timeline > li:last-child { border-bottom: none; }`
+ border-bottom: 1px solid $white-normal;
+ }
+
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
- display: inline-block;
- height: 40px;
- width: 40px;
- border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
@@ -126,13 +114,13 @@ ul.notes {
.note-awards {
.js-awards-block {
- margin-bottom: 16px;
+ margin-top: 16px;
}
}
.note-header {
- @media (max-width: $screen-xs-min) {
+ @include notes-media('max', $screen-xs-min) {
.inline {
display: block;
}
@@ -161,10 +149,10 @@ ul.notes {
.system-note {
font-size: 14px;
- padding: 0;
+ padding-left: 0;
clear: both;
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
@@ -198,11 +186,22 @@ ul.notes {
}
}
- .timeline-content {
- padding: 14px 10px;
+ .timeline-icon {
+ float: left;
- @media (min-width: $screen-sm-min) {
- margin-left: 20px;
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $gray-darkest;
+ position: absolute;
+ left: 0;
+ top: 2px;
+ }
+ }
+
+ .timeline-content {
+ @include notes-media('min', $screen-sm-min) {
+ margin-left: 30px;
}
}
@@ -371,7 +370,7 @@ ul.notes {
display: flex;
justify-content: space-between;
- @media (max-width: $screen-xs-max) {
+ @include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
@@ -386,7 +385,7 @@ ul.notes {
}
.note-header-author-name {
- @media (max-width: $screen-xs-max) {
+ @include notes-media('max', $screen-xs-max) {
display: none;
}
}
@@ -394,7 +393,7 @@ ul.notes {
.note-headline-light {
display: inline;
- @media (max-width: $screen-xs-min) {
+ @include notes-media('max', $screen-xs-min) {
display: block;
}
}
@@ -436,7 +435,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @media (max-width: $screen-xs-max) {
+ @include notes-media('max', $screen-xs-max) {
float: none;
margin-left: 0;
}
@@ -447,7 +446,7 @@ ul.notes {
}
.discussion-actions {
- @media (max-width: $screen-md-max) {
+ @include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
@@ -461,7 +460,7 @@ ul.notes {
display: inline;
line-height: 20px;
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
@@ -596,10 +595,15 @@ ul.notes {
.discussion-body,
.diff-file {
.notes .note {
- padding: 10px 15px;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
&.system-note {
- padding: 0;
+ padding-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 70px;
+ }
}
}
}
@@ -613,17 +617,11 @@ ul.notes {
}
.disabled-comment {
- margin-left: -$gl-padding-top;
- margin-right: -$gl-padding-top;
background-color: $gray-light;
border-radius: $border-radius-base;
border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color;
- line-height: 200px;
-
- .disabled-comment-text {
- line-height: normal;
- }
+ padding: 90px 0;
a {
color: $gl-link-color;
@@ -631,7 +629,7 @@ ul.notes {
}
.line-resolve-all-container {
- @media (min-width: $screen-sm-min) {
+ @include notes-media('min', $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
@@ -673,7 +671,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 6px 10px;
+ padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@@ -686,6 +684,10 @@ ul.notes {
.line-resolve-btn {
margin-right: 5px;
+
+ svg {
+ vertical-align: middle;
+ }
}
}
@@ -722,6 +724,10 @@ ul.notes {
}
}
+.line-resolve-text {
+ vertical-align: middle;
+}
+
.discussion-next-btn {
svg {
margin: 0;
@@ -738,10 +744,6 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
- // Diff is side by side
- .notes_content.parallel .note-header .note-header-author-name {
- display: block;
- }
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 292584eba28..cf2e565dd2d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -88,6 +88,10 @@
}
}
+ .btn .text-center {
+ display: inline;
+ }
+
.tooltip {
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f0bf3d4c267..24ab2bedea2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -247,7 +247,6 @@
font-size: 13px;
font-weight: 600;
line-height: 13px;
- padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 6px 14px;
text-align: center;
@@ -384,10 +383,6 @@ a.deploy-project-label {
}
}
-.last-push-widget {
- margin-top: -1px;
-}
-
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index e2f5aa8508e..907717dcb96 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active
@users = @users.reorder(:name)
- @users = @users.page(params[:page])
+ @users = @users.page(params[:page]).per(params[:per_page])
if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 5a1efcab1a3..3d49ea97591 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
- format.html { @last_push = current_user.recent_push }
+ format.html
format.atom do
load_events
render layout: false
@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
- @last_push = current_user.recent_push
@groups = []
respond_to do |format|
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 79d420a32d3..6195121b931 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
def activity
- @last_push = current_user.recent_push
-
respond_to do |format|
format.html
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 965ced4d372..18a2d69db29 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
+ return not_found unless Group.supports_nested_groups?
+
@nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController
def user_actions
if current_user
- @last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 98bbcfaaba5..654aa1a6533 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
+ def project_search_tabs?(tab)
+ abilities = Array(search_tab_ability_map[tab])
+
+ abilities.any? { |ability| can?(current_user, ability, @project) }
+ end
+
def project_nav_tab?(name)
project_nav_tabs.include? name
end
@@ -116,6 +122,7 @@ module ProjectsHelper
def last_push_event
return unless current_user
+ return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
@@ -203,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry
end
- tab_ability_map = {
+ tab_ability_map.each do |tab, ability|
+ if can?(current_user, ability, project)
+ nav_tabs << tab
+ end
+ end
+
+ nav_tabs.flatten
+ end
+
+ def tab_ability_map
+ {
environments: :read_environment,
milestones: :read_milestone,
pipelines: :read_pipeline,
@@ -215,14 +232,15 @@ module ProjectsHelper
team: :read_project_member,
wiki: :read_wiki
}
+ end
- tab_ability_map.each do |tab, ability|
- if can?(current_user, ability, project)
- nav_tabs << tab
- end
- end
-
- nav_tabs.flatten
+ def search_tab_ability_map
+ @search_tab_ability_map ||= tab_ability_map.merge(
+ blobs: :download_code,
+ commits: :download_code,
+ merge_requests: :read_merge_request,
+ notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
+ )
end
def project_lfs_status(project)
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 09b73eee8cf..c0763a8a9c4 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
+ project.rstrip!
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index cf6e53c4ca4..07213ca608a 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
- validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
- validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
- validates :ref, presence: { unless: :importing_or_inactive? }
+ validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
+ validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
+ validates :ref, presence: { unless: :importing? }
validates :description, presence: true
before_save :set_next_run_at
@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false)
end
- def importing_or_inactive?
- importing? || inactive?
- end
-
def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project)
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index c4463abdfe6..63d02b76f6b 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
-
- # Builds a relation to find multiple objects that are nested under user membership
- #
- # Usage:
- #
- # Klass.member_descendants(1)
- #
- # Returns an ActiveRecord::Relation.
- def member_descendants(user_id)
- joins(:route).
- joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
- INNER JOIN members ON members.source_id = r2.source_id
- AND members.source_type = r2.source_type").
- where('members.user_id = ?', user_id)
- end
-
- # Builds a relation to find multiple objects that are nested under user
- # membership. Includes the parent, as opposed to `#member_descendants`
- # which only includes the descendants.
- #
- # Usage:
- #
- # Klass.member_self_and_descendants(1)
- #
- # Returns an ActiveRecord::Relation.
- def member_self_and_descendants(user_id)
- joins(:route).
- joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
- OR routes.path = r2.path
- INNER JOIN members ON members.source_id = r2.source_id
- AND members.source_type = r2.source_type").
- where('members.user_id = ?', user_id)
- end
-
- # Returns all objects in a hierarchy, where any node in the hierarchy is
- # under the user membership.
- #
- # Usage:
- #
- # Klass.member_hierarchy(1)
- #
- # Examples:
- #
- # Given the following group tree...
- #
- # _______group_1_______
- # | |
- # | |
- # nested_group_1 nested_group_2
- # | |
- # | |
- # nested_group_1_1 nested_group_2_1
- #
- #
- # ... the following results are returned:
- #
- # * the user is a member of group 1
- # => 'group_1',
- # 'nested_group_1', nested_group_1_1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # * the user is a member of nested_group_2
- # => 'group1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # * the user is a member of nested_group_2_1
- # => 'group1',
- # 'nested_group_2', 'nested_group_2_1'
- #
- # Returns an ActiveRecord::Relation.
- def member_hierarchy(user_id)
- paths = member_self_and_descendants(user_id).pluck('routes.path')
-
- return none if paths.empty?
-
- wheres = paths.map do |path|
- "#{connection.quote(path)} = routes.path
- OR
- #{connection.quote(path)} LIKE CONCAT(routes.path, '/%')"
- end
-
- joins(:route).where(wheres.join(' OR '))
- end
end
def full_name
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 50a1d7fc3e1..58194b0ea13 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods
def select_for_project_authorization
- select("members.user_id, projects.id AS project_id, members.access_level")
+ select("projects.id AS project_id, members.access_level")
+ end
+
+ def select_as_master_for_project_authorization
+ select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 6aab477f431..be944da5a67 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement
class << self
+ def supports_nested_groups?
+ Gitlab::Database.postgresql?
+ end
+
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false)
- .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c06bfe0ccdd..b04bed4c014 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id)
+ User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end
def self.sort(method)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4d59267f71d..aebee06d560 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
- # Scopes the model on ancestors of the record
+ # Returns all the ancestors of the current namespaces.
def ancestors
- if parent_id
- path = route ? route.path : full_path
- paths = []
+ return self.class.none unless parent_id
- until path.blank?
- path = path.rpartition('/').first
- paths << path
- end
-
- self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
- else
- self.class.none
- end
+ Gitlab::GroupHierarchy.
+ new(self.class.where(id: parent_id)).
+ base_and_ancestors
end
- # Scopes the model on direct and indirect children of the record
+ # Returns all the descendants of the current namespace.
def descendants
- self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC')
+ Gitlab::GroupHierarchy.
+ new(self.class.where(parent_id: id)).
+ base_and_descendants
end
def user_ids_for_project_authorizations
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 4c7f4f5a429..def09675253 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+ def self.select_from_union(union)
+ select(['project_id', 'MAX(access_level) AS access_level']).
+ from("(#{union.to_sql}) #{ProjectAuthorization.table_name}").
+ group(:project_id)
+ end
+
def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple|
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index a91a986e195..fe869623833 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
validates :url, url: true, presence: true, if: :activated?
+ validates :api_url, url: true, allow_blank: true
validates :project_key, presence: true, if: :activated?
- prop_accessor :username, :password, :url, :project_key,
+ prop_accessor :username, :password, :url, :api_url, :project_key,
:jira_issue_transition_id, :title, :description
before_update :reset_password
@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService
super do
self.properties = {
title: issues_tracker['title'],
- url: issues_tracker['url']
+ url: issues_tracker['url'],
+ api_url: issues_tracker['api_url']
}
end
end
def reset_password
- # don't reset the password if a new one is provided
- if url_changed? && !password_touched?
- self.password = nil
- end
+ self.password = nil if reset_password?
end
def options
- url = URI.parse(self.url)
+ url = URI.parse(client_url)
{
username: self.username,
@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService
def fields
[
- { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService
end
def test_settings
- return unless url.present?
+ return unless client_url.present?
# Test settings by getting the project
jira_request { jira_project.present? }
end
@@ -236,13 +236,13 @@ class JiraService < IssueTrackerService
end
def send_message(issue, message, remote_link_props)
- return unless url.present?
+ return unless client_url.present?
jira_request do
if issue.comments.build.save!(body: message)
remote_link = issue.remotelink.build
remote_link.save!(remote_link_props)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
end
Rails.logger.info(result_message)
@@ -295,7 +295,20 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
nil
end
+
+ def client_url
+ api_url.present? ? api_url : url
+ end
+
+ def reset_password?
+ # don't reset the password if a new one is provided
+ return false if password_touched?
+ return true if api_url_changed?
+ return false if api_url.present?
+
+ url_changed?
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9b0c1ebd7c5..3f816a250c2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable
include CaseSensitivity
include TokenAuthenticatable
+ include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating
+ ignore_column :authorized_projects_populated
+
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
- scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- def nested_groups
- Group.member_descendants(id)
- end
-
+ # Returns a relation of groups the user has access to, including their parent
+ # and child groups (recursively).
def all_expanded_groups
- Group.member_hierarchy(id)
+ Gitlab::GroupHierarchy.new(groups).all_groups
end
def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true)
end
- def nested_groups_projects
- Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
- member_descendants(id)
- end
-
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all
end
- def set_authorized_projects_column
- unless authorized_projects_populated
- update_column(:authorized_projects_populated, true)
- end
- end
-
def authorized_projects(min_access_level = nil)
- refresh_authorized_projects unless authorized_projects_populated
-
- # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ # We're overriding an association, so explicitly call super with no
+ # arguments or it would be passed as `force_reload` to the association
projects = super()
- projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ if min_access_level
+ projects = projects.
+ where('project_authorizations.access_level >= ?', min_access_level)
+ end
projects
end
@@ -919,13 +911,13 @@ class User < ActiveRecord::Base
end
def assigned_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index b3247ae36dd..f7eb75395b5 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
+ expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
merge_request.project.archived?
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index f1030912c68..85c616ca576 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -28,6 +28,7 @@ module Issues
notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
+ invalidate_cache_counts(issue.assignees, issue)
end
issue
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 40fbe354492..80ea6312768 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -8,6 +8,7 @@ module Issues
create_note(issue)
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
+ invalidate_cache_counts(issue.assignees, issue)
end
issue
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f2053bda83a..2ffc989ed71 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index e8fb1b59752..f0d998731d7 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
private
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 54b19e6d651..f2fddf7f345 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -10,6 +10,7 @@ module MergeRequests
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
+ invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 22736c71725..1d4d03a8b7d 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -12,7 +12,7 @@ class SearchService
@project =
if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id])
- can?(current_user, :download_code, the_project) ? the_project : nil
+ can?(current_user, :read_project, the_project) ? the_project : nil
else
nil
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 8f6f5b937c4..3e07b811027 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
- return if remove.empty? && add.empty? && user.authorized_projects_populated
+ return if remove.empty? && add.empty?
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
- user.set_authorized_projects_column
end
# Since we batch insert authorization rows, Rails' associations may get
@@ -101,38 +100,13 @@ module Users
end
def fresh_authorizations
- ProjectAuthorization.
- unscoped.
- select('project_id, MAX(access_level) AS access_level').
- from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
- group(:project_id)
- end
-
- private
-
- # Returns a union query of projects that the user is authorized to access
- def project_authorizations_union
- relations = [
- # Personal projects
- user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
-
- # Projects the user is a member of
- user.projects.select_for_project_authorization,
-
- # Projects of groups the user is a member of
- user.groups_projects.select_for_project_authorization,
-
- # Projects of subgroups of groups the user is a member of
- user.nested_groups_projects.select_for_project_authorization,
-
- # Projects shared with groups the user is a member of
- user.groups.joins(:shared_projects).select_for_project_authorization,
-
- # Projects shared with subgroups of groups the user is a member of
- user.nested_groups.joins(:shared_projects).select_for_project_authorization
- ]
+ klass = if Group.supports_nested_groups?
+ Gitlab::ProjectAuthorizations::WithNestedGroups
+ else
+ Gitlab::ProjectAuthorizations::WithoutNestedGroups
+ end
- Gitlab::SQL::Union.new(relations)
+ klass.new(user).calculate
end
end
end
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 2e5f120c4e4..9b9559c7fe5 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -31,3 +31,8 @@
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
+ .col-sm-4
+ .light-well
+ %h4 Uptime
+ .data
+ %h1= time_ago_with_tooltip(Rails.application.config.booted_at)
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index e1b270a08c2..a676eba2aee 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,6 +1,3 @@
-.hidden-xs
- = render "events/event_last_push", event: @last_push
-
.nav-block.activities
.controls
= link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 190ad4b40a5..f893c3e1675 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,10 +1,16 @@
+- @no_container = true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
-= render 'dashboard/activity_head'
+.hidden-xs
+ = render "projects/last_push"
+
+%div{ class: container_class }
+ = render 'dashboard/activity_head'
-%section.activities
- = render 'activities'
+ %section.activities
+ = render 'activities'
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 596499230f9..3b2a555a143 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,19 +1,21 @@
+- @no_container = true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-- unless show_user_callout?
- = render 'shared/user_callout'
+= render "projects/last_push"
-- if @projects.any? || params[:name]
- = render 'dashboard/projects_head'
+%div{ class: container_class }
+ - unless show_user_callout?
+ = render 'shared/user_callout'
-- if @last_push
- = render "events/event_last_push", event: @last_push
+ - if @projects.any? || params[:name]
+ = render 'dashboard/projects_head'
-- if @projects.any? || params[:name]
- = render 'projects'
-- else
- = render "zero_authorized_projects"
+ - if @projects.any? || params[:name]
+ = render 'projects'
+ - else
+ = render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 162ae153b1c..99efe9c9b86 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,13 +1,15 @@
+- @no_container = true
+
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
-= render 'dashboard/projects_head'
+= render "projects/last_push"
-- if @last_push
- = render "events/event_last_push", event: @last_push
+%div{ class: container_class }
+ = render 'dashboard/projects_head'
-- if @projects.any? || params[:filter_projects]
- = render 'projects'
-- else
- %h3 You don't have starred projects yet
- %p.slead Visit project page and press on star icon and it will appear on this page.
+ - if @projects.any? || params[:filter_projects]
+ = render 'projects'
+ - else
+ %h3 You don't have starred projects yet
+ %p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 7ba3f3f6c42..db5ab939948 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,10 +1,11 @@
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- .flash-container
- - if current_user
- .discussion-reply-holder
+ .flash-container
+
+ .discussion-reply-holder
+ - if can_create_note?
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
@@ -19,3 +20,10 @@
= render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
+ - elsif !current_user
+ .disabled-comment.text-center
+ Please
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ or
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
+ to reply
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
deleted file mode 100644
index 1584695a62b..00000000000
--- a/app/views/events/_event_last_push.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- if show_last_push_widget?(event)
- .row-content-block.clear-block.last-push-widget
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), title: h(event.project.name) do
- %strong= event.ref_name
- %span at
- %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
-
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
- Create merge request
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index c0943100ae3..769ac655d0a 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -7,7 +7,7 @@
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
- = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link
+ = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index d7851c79990..fd6e7111f38 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,6 +1,3 @@
-.hidden-xs
- = render "events/event_last_push", event: @last_push
-
.nav-block
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml
index 873504099d4..0f63774fb9b 100644
--- a/app/views/groups/_head.html.haml
+++ b/app/views/groups/_head.html.haml
@@ -12,3 +12,6 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
+
+.hidden-xs
+ = render "projects/last_push"
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
index b2097e88741..35b75bc0923 100644
--- a/app/views/groups/_show_nav.html.haml
+++ b/app/views/groups/_show_nav.html.haml
@@ -2,6 +2,7 @@
= nav_link(page: group_path(@group)) do
= link_to group_path(@group) do
Projects
- = nav_link(page: subgroups_group_path(@group)) do
- = link_to subgroups_group_path(@group) do
- Subgroups
+ - if Group.supports_nested_groups?
+ = nav_link(page: subgroups_group_path(@group)) do
+ = link_to subgroups_group_path(@group) do
+ Subgroups
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 18997baa998..80a8ba4a755 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,7 +6,6 @@
= render 'groups/head'
= render 'groups/home_panel'
-
.groups-header{ class: container_class }
.top-area
= render 'groups/show_nav'
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index f6132464910..86779eeaf15 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -5,7 +5,7 @@
.fade-right
= icon('angle-right')
%ul.nav-links.scrolling-tabs
- = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index f5bb7364d4a..10f581d751b 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,5 +1,3 @@
-- @no_container = true
-
%div{ class: container_class }
.nav-block.activity-filter-block.activities
.controls
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index f8a6e98d280..e8b1940af2d 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,18 +1,18 @@
-- if event = last_push_event
- - if show_last_push_widget?(event)
- .row-content-block.top-block.hidden-xs.white
- %div{ class: container_class }
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name, class: 'commit-sha') do
- %strong= event.ref_name
- - if @project && event.project != @project
- %span at
- %strong= link_to_project event.project
- = clipboard_button(text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard')
- #{time_ago_with_tooltip(event.created_at)}
+- event = last_push_event
+- if event && show_last_push_widget?(event)
+ .row-content-block.top-block.hidden-xs.white
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ %strong
+ = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name), class: 'ref-name'
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
- Create merge request
+ - if event.project != @project
+ %span at
+ %strong= link_to_project event.project
+
+ #{time_ago_with_tooltip(event.created_at)}
+
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
+ Create merge request
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 27c8e3c7fca..ef8d8051cbf 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- page_title "Activity"
= render "projects/head"
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 67f57b5e4b9..41f75a491a5 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,13 +1,14 @@
- @no_container = true
+
- page_title @blob.path, @ref
= render "projects/commits/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('blob')
-%div{ class: container_class }
- = render 'projects/last_push'
+= render 'projects/last_push'
+%div{ class: container_class }
#tree-holder.tree-holder
= render 'blob', blob: @blob
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 502220232a1..2cb3045f83e 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -5,12 +5,14 @@
- unless @project.default_issues_tracker?
= content_for :sub_nav do
= render "projects/merge_requests/head"
-= render 'projects/last_push'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
+
+= render 'projects/last_push'
+
- if @project.merge_requests.exists?
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 082a6bcbb2a..7bde839e26f 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -4,7 +4,8 @@
= pipeline_schedule.description
%td.branch-name-cell
= icon('code-fork')
- = link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
+ - if pipeline_schedule.ref
+ = link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td
- if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml
index edc4f7b079f..0b7e3d22dd7 100644
--- a/app/views/projects/pipelines/charts/_overall.haml
+++ b/app/views/projects/pipelines/charts/_overall.haml
@@ -2,13 +2,13 @@
%ul
%li
Total:
- %strong= pluralize @project.builds.count(:all), 'build'
+ %strong= pluralize @project.builds.count(:all), 'job'
%li
Successful:
- %strong= pluralize @project.builds.success.count(:all), 'build'
+ %strong= pluralize @project.builds.success.count(:all), 'job'
%li
Failed:
- %strong= pluralize @project.builds.failed.count(:all), 'build'
+ %strong= pluralize @project.builds.failed.count(:all), 'job'
%li
Success ratio:
%strong
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index b51955010ce..f7e410e27b8 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -4,6 +4,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= render "projects/commits/head"
+
= render 'projects/last_push'
%div{ class: container_class }
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 059a0d1ac78..314d8e9cb25 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -3,41 +3,48 @@
.fade-right= icon('angle-right')
%ul.nav-links.search-filter.scrolling-tabs
- if @project
- %li{ class: active_when(@scope == 'blobs') }
- = link_to search_filter_path(scope: 'blobs') do
- Code
- %span.badge
- = @search_results.blobs_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
- %li{ class: active_when(@scope == 'notes') }
- = link_to search_filter_path(scope: 'notes') do
- Comments
- %span.badge
- = @search_results.notes_count
- %li{ class: active_when(@scope == 'wiki_blobs') }
- = link_to search_filter_path(scope: 'wiki_blobs') do
- Wiki
- %span.badge
- = @search_results.wiki_blobs_count
- %li{ class: active_when(@scope == 'commits') }
- = link_to search_filter_path(scope: 'commits') do
- Commits
- %span.badge
- = @search_results.commits_count
+ - if project_search_tabs?(:blobs)
+ %li{ class: active_when(@scope == 'blobs') }
+ = link_to search_filter_path(scope: 'blobs') do
+ Code
+ %span.badge
+ = @search_results.blobs_count
+ - if project_search_tabs?(:issues)
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ - if project_search_tabs?(:merge_requests)
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ - if project_search_tabs?(:milestones)
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
+ - if project_search_tabs?(:notes)
+ %li{ class: active_when(@scope == 'notes') }
+ = link_to search_filter_path(scope: 'notes') do
+ Comments
+ %span.badge
+ = @search_results.notes_count
+ - if project_search_tabs?(:wiki)
+ %li{ class: active_when(@scope == 'wiki_blobs') }
+ = link_to search_filter_path(scope: 'wiki_blobs') do
+ Wiki
+ %span.badge
+ = @search_results.wiki_blobs_count
+ - if project_search_tabs?(:commits)
+ %li{ class: active_when(@scope == 'commits') }
+ = link_to search_filter_path(scope: 'commits') do
+ Commits
+ %span.badge
+ = @search_results.commits_count
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 93c7fa0c7d6..1cf662e29c4 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -9,7 +9,7 @@
- selected = local_assigns.fetch(:selected, nil)
- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index d23f79be2be..271150ed318 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -5,3 +5,13 @@
-# This check is duplicated below, to avoid conflicts with EE.
- return unless issuable.can_remove_source_branch?(current_user)
+
+.form-group
+ .col-sm-10.col-sm-offset-2
+ - if issuable.can_remove_source_branch?(current_user)
+ .checkbox
+ - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 785b1b22a49..5902798dfd0 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -3,24 +3,23 @@
= render 'shared/notes/edit_form', project: @project
-%ul.notes.notes-form.timeline
- %li.timeline-entry
- .flash-container.timeline-content
+- if can_create_note?
+ %ul.notes.notes-form.timeline
+ %li.timeline-entry
+ .flash-container.timeline-content
- - if can_create_note?
.timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view
- - elsif !current_user
- .disabled-comment.text-center
- .disabled-comment-text.inline
- Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
- or
- = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
- to post a comment
+- elsif !current_user
+ .disabled-comment.text-center.prepend-top-default
+ Please
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ or
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
+ to comment
:javascript
var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}", #{autocomplete})
diff --git a/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml
new file mode 100644
index 00000000000..b350b27d863
--- /dev/null
+++ b/changelogs/unreleased/23036-replace-all-spinach-tests-with-rspec-feature-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Replace 'starred_projects.feature' spinach test with an rspec analog
+merge_request: 11752
+author: blackst0ne
diff --git a/changelogs/unreleased/27439-memory-usage-info.yml b/changelogs/unreleased/27439-memory-usage-info.yml
new file mode 100644
index 00000000000..dd212853f57
--- /dev/null
+++ b/changelogs/unreleased/27439-memory-usage-info.yml
@@ -0,0 +1,4 @@
+---
+title: Add performance deltas between app deployments on Merge Request widget
+merge_request: 11730
+author:
diff --git a/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml b/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml
new file mode 100644
index 00000000000..c9bd2dc465e
--- /dev/null
+++ b/changelogs/unreleased/30917-wiki-is-not-searchable-with-guest-permissions.yml
@@ -0,0 +1,4 @@
+---
+title: 'Fix: Wiki is not searchable with Guest permissions'
+merge_request:
+author:
diff --git a/changelogs/unreleased/31448-jira-urls.yml b/changelogs/unreleased/31448-jira-urls.yml
new file mode 100644
index 00000000000..d0e39f61b55
--- /dev/null
+++ b/changelogs/unreleased/31448-jira-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Add API URL to JIRA settings
+merge_request:
+author:
diff --git a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml
new file mode 100644
index 00000000000..4137050a077
--- /dev/null
+++ b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Fix the last coverage in trace log should be extracted
+merge_request: 11128
+author: dosuken123
diff --git a/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml b/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml
new file mode 100644
index 00000000000..6dc48d6b2d8
--- /dev/null
+++ b/changelogs/unreleased/31616-add-uptime-of-gitlab-instance-in-admin-area.yml
@@ -0,0 +1,4 @@
+---
+title: Add server uptime to System Info page in admin dashboard
+merge_request: 11590
+author: Justin Boltz
diff --git a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml
new file mode 100644
index 00000000000..a58f3a7429e
--- /dev/null
+++ b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline_schedules pages throwing error 500
+merge_request: 11706
+author: dosuken123
diff --git a/changelogs/unreleased/32851-postgres-min-version.yml b/changelogs/unreleased/32851-postgres-min-version.yml
new file mode 100644
index 00000000000..139307d65c6
--- /dev/null
+++ b/changelogs/unreleased/32851-postgres-min-version.yml
@@ -0,0 +1,4 @@
+---
+title: Minimum postgresql version is now 9.2
+merge_request: 11677
+author:
diff --git a/changelogs/unreleased/ci-build-pipeline-header-vue.yml b/changelogs/unreleased/ci-build-pipeline-header-vue.yml
new file mode 100644
index 00000000000..2bbff2fdd16
--- /dev/null
+++ b/changelogs/unreleased/ci-build-pipeline-header-vue.yml
@@ -0,0 +1,4 @@
+---
+title: Creates CI Header component for Pipelines and Jobs details pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-consistent-last-push-event.yml b/changelogs/unreleased/dm-consistent-last-push-event.yml
new file mode 100644
index 00000000000..acc17cb4523
--- /dev/null
+++ b/changelogs/unreleased/dm-consistent-last-push-event.yml
@@ -0,0 +1,4 @@
+---
+title: Consistently display last push event widget
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml
new file mode 100644
index 00000000000..e40668546c0
--- /dev/null
+++ b/changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml
@@ -0,0 +1,4 @@
+---
+title: Fix counter cache for acts as taggable
+merge_request:
+author:
diff --git a/changelogs/unreleased/issuable-form-create-label-sub-groups.yml b/changelogs/unreleased/issuable-form-create-label-sub-groups.yml
new file mode 100644
index 00000000000..54b818d6d5e
--- /dev/null
+++ b/changelogs/unreleased/issuable-form-create-label-sub-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed create new label form in issue form not working for sub-group projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/rework-authorizations-performance.yml b/changelogs/unreleased/rework-authorizations-performance.yml
new file mode 100644
index 00000000000..f64257a6f56
--- /dev/null
+++ b/changelogs/unreleased/rework-authorizations-performance.yml
@@ -0,0 +1,6 @@
+---
+title: >
+ Project authorizations are calculated much faster when using PostgreSQL, and
+ nested groups support for MySQL has been removed
+merge_request: 10885
+author:
diff --git a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml
new file mode 100644
index 00000000000..d633995d467
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml
@@ -0,0 +1,4 @@
+---
+title: Strip trailing whitespaces in submodule URLs
+merge_request:
+author:
diff --git a/config/initializers/acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb
index c564c0cab11..54e9fcc31db 100644
--- a/config/initializers/acts_as_taggable.rb
+++ b/config/initializers/0_acts_as_taggable.rb
@@ -3,3 +3,7 @@ ActsAsTaggableOn.strict_case_match = true
# tags_counter enables caching count of tags which results in an update whenever a tag is added or removed
# since the count is not used anywhere its better performance wise to disable this cache
ActsAsTaggableOn.tags_counter = false
+
+# validate that counter cache is disabled
+raise "Counter cache is not disabled" if
+ ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache]
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
new file mode 100644
index 00000000000..7f0df8949db
--- /dev/null
+++ b/config/initializers/postgresql_cte.rb
@@ -0,0 +1,132 @@
+# Adds support for WITH statements when using PostgreSQL. The code here is taken
+# from https://github.com/shmay/ctes_in_my_pg which at the time of writing has
+# not been pushed to RubyGems. The license of this repository is as follows:
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2012 Dan McClain
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+module ActiveRecord
+ class Relation
+ class Merger # :nodoc:
+ def normal_values
+ NORMAL_VALUES + [:with]
+ end
+ end
+ end
+end
+
+module ActiveRecord::Querying
+ delegate :with, to: :all
+end
+
+module ActiveRecord
+ class Relation
+ # WithChain objects act as placeholder for queries in which #with does not have any parameter.
+ # In this case, #with must be chained with #recursive to return a new relation.
+ class WithChain
+ def initialize(scope)
+ @scope = scope
+ end
+
+ # Returns a new relation expressing WITH RECURSIVE
+ def recursive(*args)
+ @scope.with_values += args
+ @scope.recursive_value = true
+ @scope
+ end
+ end
+
+ def with_values
+ @values[:with] || []
+ end
+
+ def with_values=(values)
+ raise ImmutableRelation if @loaded
+ @values[:with] = values
+ end
+
+ def recursive_value=(value)
+ raise ImmutableRelation if @loaded
+ @values[:recursive] = value
+ end
+
+ def recursive_value
+ @values[:recursive]
+ end
+
+ def with(opts = :chain, *rest)
+ if opts == :chain
+ WithChain.new(spawn)
+ elsif opts.blank?
+ self
+ else
+ spawn.with!(opts, *rest)
+ end
+ end
+
+ def with!(opts = :chain, *rest) # :nodoc:
+ if opts == :chain
+ WithChain.new(self)
+ else
+ self.with_values += [opts] + rest
+ self
+ end
+ end
+
+ def build_arel
+ arel = super()
+
+ build_with(arel) if @values[:with]
+
+ arel
+ end
+
+ def build_with(arel)
+ with_statements = with_values.flat_map do |with_value|
+ case with_value
+ when String
+ with_value
+ when Hash
+ with_value.map do |name, expression|
+ case expression
+ when String
+ select = Arel::Nodes::SqlLiteral.new "(#{expression})"
+ when ActiveRecord::Relation, Arel::SelectManager
+ select = Arel::Nodes::SqlLiteral.new "(#{expression.to_sql})"
+ end
+ Arel::Nodes::As.new Arel::Nodes::SqlLiteral.new("\"#{name}\""), select
+ end
+ when Arel::Nodes::As
+ with_value
+ end
+ end
+
+ unless with_statements.empty?
+ if recursive_value
+ arel.with :recursive, with_statements
+ else
+ arel.with with_statements
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/server_uptime.rb b/config/initializers/server_uptime.rb
new file mode 100644
index 00000000000..46bf242e143
--- /dev/null
+++ b/config/initializers/server_uptime.rb
@@ -0,0 +1 @@
+Rails.application.config.booted_at = Time.now
diff --git a/config/webpack.config.js b/config/webpack.config.js
index d6b92ced402..c77b1d6334c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -5,6 +5,7 @@ var path = require('path');
var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
+var NameAllModulesPlugin = require('name-all-modules-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
@@ -69,7 +70,8 @@ var config = {
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
- filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
+ filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
+ chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
},
devtool: 'cheap-module-source-map',
@@ -100,7 +102,7 @@ var config = {
loader: 'file-loader',
},
{
- test: /locale\/[a-z]+\/(.*)\.js$/,
+ test: /locale\/\w+\/(.*)\.js$/,
loader: 'exports-loader?locales',
},
]
@@ -126,8 +128,20 @@ var config = {
jQuery: 'jquery',
}),
- // use deterministic module ids
+ // assign deterministic module ids
new webpack.NamedModulesPlugin(),
+ new NameAllModulesPlugin(),
+
+ // assign deterministic chunk ids
+ new webpack.NamedChunksPlugin((chunk) => {
+ if (chunk.name) {
+ return chunk.name;
+ }
+ return chunk.modules.map((m) => {
+ var chunkPath = m.request.split('!').pop();
+ return path.relative(m.context, chunkPath);
+ }).join('_');
+ }),
// create cacheable common library bundle for all vue chunks
new webpack.optimize.CommonsChunkPlugin({
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
index bd0463886bc..4d6a61bd614 100644
--- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class SetMissingStageOnCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
index 1eb99feb40c..b2a2ce41391 100644
--- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
+++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
index f1a1f001cb3..febd2c0e65e 100644
--- a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
+++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb
index 66172bda6ff..2d2725ccf59 100644
--- a/db/migrate/20160919144305_add_type_to_labels.rb
+++ b/db/migrate/20160919144305_add_type_to_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class AddTypeToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb
index a576bb7b622..fe11699c196 100644
--- a/db/migrate/20161018124658_make_project_owners_masters.rb
+++ b/db/migrate/20161018124658_make_project_owners_masters.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class MakeProjectOwnersMasters < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
index 50ad7437227..c7cada6dfc5 100644
--- a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
+++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
index 23e7500a32d..7b61e811317 100644
--- a/db/migrate/20170320173259_migrate_assignees.rb
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -1,6 +1,4 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
+# rubocop:disable Migration/UpdateColumnInBatches
class MigrateAssignees < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170503140201_reschedule_project_authorizations.rb b/db/migrate/20170503140201_reschedule_project_authorizations.rb
new file mode 100644
index 00000000000..fa45adadbae
--- /dev/null
+++ b/db/migrate/20170503140201_reschedule_project_authorizations.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 RescheduleProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ def up
+ offset = 0
+ batch = 5000
+ start = Time.now
+
+ loop do
+ relation = User.where('id > ?', offset)
+ user_ids = relation.limit(batch).reorder(id: :asc).pluck(:id)
+
+ break if user_ids.empty?
+
+ offset = user_ids.last
+
+ # This will schedule each batch 5 minutes after the previous batch was
+ # scheduled. This smears out the load over time, instead of immediately
+ # scheduling a million jobs.
+ Sidekiq::Client.push_bulk(
+ 'queue' => 'authorized_projects',
+ 'args' => user_ids.zip,
+ 'class' => 'AuthorizedProjectsWorker',
+ 'at' => start.to_i
+ )
+
+ start += 5.minutes
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
new file mode 100644
index 00000000000..c67690642c9
--- /dev/null
+++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
@@ -0,0 +1,123 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+# This migration depends on code external to it. For example, it relies on
+# updating a namespace to also rename directories (uploads, GitLab pages, etc).
+# The alternative is to copy hundreds of lines of code into this migration,
+# adjust them where needed, etc; something which doesn't work well at all.
+class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def run_migration?
+ Gitlab::Database.mysql?
+ end
+
+ def up
+ return unless run_migration?
+
+ # For all sub-groups we need to give the right people access. We do this as
+ # follows:
+ #
+ # 1. Get all the ancestors for the current namespace
+ # 2. Get all the members of these namespaces, along with their higher access
+ # level
+ # 3. Give these members access to the current namespace
+ Namespace.unscoped.where('parent_id IS NOT NULL').find_each do |namespace|
+ rows = []
+ existing = namespace.members.pluck(:user_id)
+
+ all_members_for(namespace).each do |member|
+ next if existing.include?(member[:user_id])
+
+ rows << {
+ access_level: member[:access_level],
+ source_id: namespace.id,
+ source_type: 'Namespace',
+ user_id: member[:user_id],
+ notification_level: 3, # global
+ type: 'GroupMember',
+ created_at: Time.current,
+ updated_at: Time.current
+ }
+ end
+
+ bulk_insert_members(rows)
+
+ # This method relies on the parent to determine the proper path.
+ # Because we reset "parent_id" this method will not return the right path
+ # when moving namespaces.
+ full_path_was = namespace.send(:full_path_was)
+
+ namespace.define_singleton_method(:full_path_was) { full_path_was }
+
+ namespace.update!(parent_id: nil, path: new_path_for(namespace))
+ end
+ end
+
+ def down
+ # There is no way to go back from regular groups to nested groups.
+ end
+
+ # Generates a new (unique) path for a namespace.
+ def new_path_for(namespace)
+ counter = 1
+ base = namespace.full_path.tr('/', '-')
+ new_path = base
+
+ while Namespace.unscoped.where(path: new_path).exists?
+ new_path = base + "-#{counter}"
+ counter += 1
+ end
+
+ new_path
+ end
+
+ # Returns an Array containing all the ancestors of the current namespace.
+ #
+ # This method is not particularly efficient, but it's probably still faster
+ # than using the "routes" table. Most importantly of all, it _only_ depends
+ # on the namespaces table and the "parent_id" column.
+ def ancestors_for(namespace)
+ ancestors = []
+ current = namespace
+
+ while current&.parent_id
+ # We're using find_by(id: ...) here to deal with cases where the
+ # parent_id may point to a missing row.
+ current = Namespace.unscoped.select([:id, :parent_id]).
+ find_by(id: current.parent_id)
+
+ ancestors << current.id if current
+ end
+
+ ancestors
+ end
+
+ # Returns a relation containing all the members that have access to any of
+ # the current namespace's parent namespaces.
+ def all_members_for(namespace)
+ Member.
+ unscoped.
+ select(['user_id', 'MAX(access_level) AS access_level']).
+ where(source_type: 'Namespace', source_id: ancestors_for(namespace)).
+ group(:user_id)
+ end
+
+ def bulk_insert_members(rows)
+ return if rows.empty?
+
+ keys = rows.first.keys
+
+ tuples = rows.map do |row|
+ row.map { |(_, value)| connection.quote(value) }
+ end
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO members (#{keys.join(', ')})
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+ end
+end
diff --git a/db/migrate/20170504182103_add_index_project_group_links_group_id.rb b/db/migrate/20170504182103_add_index_project_group_links_group_id.rb
new file mode 100644
index 00000000000..62bf641daa6
--- /dev/null
+++ b/db/migrate/20170504182103_add_index_project_group_links_group_id.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 AddIndexProjectGroupLinksGroupId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_group_links, :group_id
+ end
+
+ def down
+ remove_concurrent_index :project_group_links, :group_id
+ end
+end
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
index b518038e93a..82f8147547e 100644
--- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -1,6 +1,4 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
+# rubocop:disable Migration/UpdateColumnInBatches
class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
index b61dd7cfc61..b1c9eed1148 100644
--- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -1,6 +1,4 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
+# rubocop:disable Migration/UpdateColumnInBatches
class ResetRelativePositionForIssue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
index a19b73fc114..3c13a3d2518 100644
--- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
+++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateColumnInBatches
class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb b/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb
new file mode 100644
index 00000000000..1b44334395f
--- /dev/null
+++ b/db/post_migrate/20170503120310_remove_users_authorized_projects_populated.rb
@@ -0,0 +1,15 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ remove_column :users, :authorized_projects_populated, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6b513a5725..4c73f74ef1f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -928,6 +928,8 @@ ActiveRecord::Schema.define(version: 20170523091700) do
t.date "expires_at"
end
+ add_index "project_group_links", ["group_id"], name: "index_project_group_links_on_group_id", using: :btree
+
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
@@ -1355,7 +1357,6 @@ ActiveRecord::Schema.define(version: 20170523091700) do
t.boolean "external", default: false
t.string "incoming_email_token"
t.string "organization"
- t.boolean "authorized_projects_populated"
t.boolean "require_two_factor_authentication_from_group", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false
t.boolean "ghost"
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index da2dac23c6a..9a171d34671 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -281,5 +281,5 @@ GitLab database to `longtext` columns, which can persist values of up to 4GB
Details can be found in the [PostgreSQL][postgres-text-type] and
[MySQL][mysql-text-types] manuals.
-[postgres-text-type]: http://www.postgresql.org/docs/9.1/static/datatype-character.html
+[postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html
[mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 5bba405f159..cda70b78c61 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -185,7 +185,8 @@ Create a `git` user for GitLab:
We recommend using a PostgreSQL database. For MySQL check the
[MySQL setup guide](database_mysql.md).
-> **Note**: because we need to make use of extensions you need at least pgsql 9.1.
+> **Note**: because we need to make use of extensions and concurrent index removal,
+you need at least PostgreSQL 9.2.
1. Install the database packages:
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 2e456557d77..5338ccb9d3a 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -138,7 +138,7 @@ installation (e.g. the number of users, projects, etc).
### PostgreSQL Requirements
-As of GitLab 9.0, PostgreSQL 9.2 or newer is required, and earlier versions are
+As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are
not supported. We highly recommend users to use at least PostgreSQL 9.6 as this
is the PostgreSQL version used for development and testing.
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index d5edf36f6b0..c4921c74a17 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -13,6 +13,15 @@ up to 20 levels of nested groups, which among other things can help you to:
- **Make it easier to manage people and control visibility.** Give people
different [permissions][] depending on their group [membership](#membership).
+## Database Requirements
+
+Nested groups are only supported when you use PostgreSQL. Supporting nested
+groups on MySQL in an efficient way is not possible due to MySQL's limitations.
+See the following links for more information:
+
+* <https://gitlab.com/gitlab-org/gitlab-ce/issues/30472>
+* <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10885>
+
## Overview
A group can have many subgroups inside it, and at the same time a group can have
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index f611029afdc..a048260b033 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -97,7 +97,8 @@ in the table below.
| Field | Description |
| ----- | ----------- |
-| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
+| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
+| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
diff --git a/features/dashboard/starred_projects.feature b/features/dashboard/starred_projects.feature
deleted file mode 100644
index 9dfd2fbab9c..00000000000
--- a/features/dashboard/starred_projects.feature
+++ /dev/null
@@ -1,12 +0,0 @@
-@dashboard
-Feature: Dashboard Starred Projects
- Background:
- Given I sign in as a user
- And public project "Community"
- And I starred project "Community"
- And I own project "Shop"
- And I visit dashboard starred projects page
-
- Scenario: I should see projects list
- Then I should see project "Community"
- And I should not see project "Shop"
diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature
index c45ed9ea68b..2ab1c19f452 100644
--- a/features/project/merge_requests/accept.feature
+++ b/features/project/merge_requests/accept.feature
@@ -7,6 +7,7 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request and removing the source branch
Given I am on the Merge Request detail page
+ When I check the "Remove source branch" option
And I click on Accept Merge Request
Then I should see merge request merged
And I should not see the Remove Source Branch button
@@ -14,6 +15,7 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request when URL has an anchor
Given I am on the Merge Request detail with note anchor page
+ When I check the "Remove source branch" option
And I click on Accept Merge Request
Then I should see merge request merged
And I should not see the Remove Source Branch button
@@ -21,7 +23,6 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request without removing the source branch
Given I am on the Merge Request detail page
- When I click on "Remove source branch" option
When I click on Accept Merge Request
Then I should see merge request merged
And I should see the Remove Source Branch button
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index bf09d7b7114..71c69a4fdea 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -22,7 +22,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I click "Create merge request" link' do
- click_link "Create merge request"
+ find_link("Create merge request", visible: false).trigger('click')
end
step 'I see prefilled new Merge Request page' do
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index 023f9bef8e5..870dc862992 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -11,10 +11,14 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
visit merge_request_path(@merge_request, anchor: 'note_123')
end
- step 'I click on "Remove source branch" option' do
+ step 'I uncheck the "Remove source branch" option' do
uncheck('Remove source branch')
end
+ step 'I check the "Remove source branch" option' do
+ check('Remove source branch')
+ end
+
step 'I click on Accept Merge Request' do
click_button('Merge')
end
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 3c0d987e403..66368a159ec 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -178,7 +178,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill jira settings' do
- fill_in 'URL', with: 'http://jira.example'
+ fill_in 'Web URL', with: 'http://jira.example'
+ fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab'
fill_in 'Project Key', with: 'GITLAB'
@@ -186,7 +187,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I should see jira service settings saved' do
- expect(find_field('URL').value).to eq 'http://jira.example'
+ expect(find_field('Web URL').value).to eq 'http://jira.example'
+ expect(find_field('JIRA API URL').value).to eq 'http://jira.example/api'
expect(find_field('Username').value).to eq 'gitlab'
expect(find_field('Project Key').value).to eq 'GITLAB'
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index cf4affe1758..8c5e5c91769 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -152,7 +152,10 @@ module API
expose :web_url
expose :request_access_enabled
expose :full_name, :full_path
- expose :parent_id
+
+ if ::Group.supports_nested_groups?
+ expose :parent_id
+ end
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 3da7d735da8..ee85b777aff 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -70,7 +70,11 @@ module API
params do
requires :name, type: String, desc: 'The name of the group'
requires :path, type: String, desc: 'The path of the group'
- optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+
+ if ::Group.supports_nested_groups?
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+ end
+
use :optional_params
end
post do
diff --git a/lib/api/services.rb b/lib/api/services.rb
index cb07df9e249..47bd9940f77 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -304,7 +304,13 @@ module API
required: true,
name: :url,
type: String,
- desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com'
+ },
+ {
+ required: false,
+ name: :api_url,
+ type: String,
+ desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
},
{
required: true,
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 332f233bf5e..2e1b243c2db 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -137,7 +137,10 @@ module API
expose :web_url
expose :request_access_enabled
expose :full_name, :full_path
- expose :parent_id
+
+ if ::Group.supports_nested_groups?
+ expose :parent_id
+ end
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 6187445fc8d..2c52d21fa1c 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -74,7 +74,11 @@ module API
params do
requires :name, type: String, desc: 'The name of the group'
requires :path, type: String, desc: 'The path of the group'
- optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+
+ if ::Group.supports_nested_groups?
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
+ end
+
use :optional_params
end
post do
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index fa462cbe095..c4c0623df6c 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -73,7 +73,7 @@ module Gitlab
match = ""
- stream.each_line do |line|
+ reverse_line do |line|
matches = line.scan(regex)
next unless matches.is_a?(Array)
next if matches.empty?
@@ -86,34 +86,39 @@ module Gitlab
nil
rescue
# if bad regex or something goes wrong we dont want to interrupt transition
- # so we just silentrly ignore error for now
+ # so we just silently ignore error for now
end
private
- def read_last_lines(last_lines)
- chunks = []
- pos = lines = 0
- max = stream.size
-
- # We want an extra line to make sure fist line has full contents
- while lines <= last_lines && pos < max
- pos += BUFFER_SIZE
-
- buf =
- if pos <= max
- stream.seek(-pos, IO::SEEK_END)
- stream.read(BUFFER_SIZE)
- else # Reached the head, read only left
- stream.seek(0)
- stream.read(BUFFER_SIZE - (pos - max))
- end
-
- lines += buf.count("\n")
- chunks.unshift(buf)
+ def read_last_lines(limit)
+ to_enum(:reverse_line).first(limit).reverse.join
+ end
+
+ def reverse_line
+ stream.seek(0, IO::SEEK_END)
+ debris = ''
+
+ until (buf = read_backward(BUFFER_SIZE)).empty?
+ buf += debris
+ debris, *lines = buf.each_line.to_a
+ lines.reverse_each do |line|
+ yield(line.force_encoding('UTF-8'))
+ end
end
- chunks.join.lines.last(last_lines).join
+ yield(debris.force_encoding('UTF-8')) unless debris.empty?
+ end
+
+ def read_backward(length)
+ cur_offset = stream.tell
+ start = cur_offset - length
+ start = 0 if start < 0
+
+ stream.seek(start, IO::SEEK_SET)
+ stream.read(cur_offset - start).tap do
+ stream.seek(start, IO::SEEK_SET)
+ end
end
end
end
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
index 7ba0b3c39b7..7bbd154eb03 100644
--- a/lib/gitlab/dependency_linker/base_linker.rb
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -1,8 +1,8 @@
module Gitlab
module DependencyLinker
class BaseLinker
- URL_REGEX = %r{https?://[^'"]+}.freeze
- REPO_REGEX = %r{[^/'"]+/[^/'"]+}.freeze
+ URL_REGEX = %r{https?://[^'" ]+}.freeze
+ REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
class_attribute :file_type
@@ -69,7 +69,7 @@ module Gitlab
@highlighted_lines ||= highlighted_text.lines
end
- def regexp_for_value(value, default: /[^'"]+/)
+ def regexp_for_value(value, default: /[^'" ]+/)
case value
when Array
Regexp.union(value.map { |v| regexp_for_value(v, default: default) })
diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb
index 1b1ca000977..a8ef25233d8 100644
--- a/lib/gitlab/dependency_linker/json_linker.rb
+++ b/lib/gitlab/dependency_linker/json_linker.rb
@@ -24,8 +24,8 @@ module Gitlab
# link_json('specific_package', '1.0.1', link: :key)
# # Will link `specific_package` in `"specific_package": "1.0.1"`
def link_json(key, value = nil, link: :value, &url_proc)
- key = regexp_for_value(key, default: /[^"]+/)
- value = regexp_for_value(value, default: /[^"]+/)
+ key = regexp_for_value(key, default: /[^" ]+/)
+ value = regexp_for_value(value, default: /[^" ]+/)
if link == :value
value = /(?<name>#{value})/
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index bce3aef8bd3..898a5ae15f2 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -19,22 +19,19 @@ module Gitlab
@line_count = 0
@byte_count = 0
@overflow = false
+ @empty = true
@array = Array.new
end
def each(&block)
- if @populated
- # @iterator.each is slower than just iterating the array in place
- @array.each(&block)
- else
- Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
- each_patch(&block)
- end
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+ each_patch(&block)
end
end
def empty?
- !@iterator.any?
+ any? # Make sure the iterator has been exercised
+ @empty
end
def overflow?
@@ -60,7 +57,6 @@ module Gitlab
collection = each_with_index do |element, i|
@array[i] = yield(element)
end
- @populated = true
collection
end
@@ -72,7 +68,6 @@ module Gitlab
return if @populated
each { nil } # force a loop through all diffs
- @populated = true
nil
end
@@ -81,15 +76,17 @@ module Gitlab
end
def each_patch
- @iterator.each_with_index do |raw, i|
- # First yield cached Diff instances from @array
- if @array[i]
- yield @array[i]
- next
- end
+ i = 0
+ @array.each do |diff|
+ yield diff
+ i += 1
+ end
+
+ return if @overflow
+ return if @iterator.nil?
- # We have exhausted @array, time to create new Diff instances or stop.
- break if @overflow
+ @iterator.each do |raw|
+ @empty = false
if !@all_diffs && i >= @max_files
@overflow = true
@@ -115,7 +112,13 @@ module Gitlab
end
yield @array[i] = diff
+ i += 1
end
+
+ @populated = true
+
+ # Allow iterator to be garbage-collected. It cannot be reused anyway.
+ @iterator = nil
end
end
end
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
new file mode 100644
index 00000000000..e9d5d52cabb
--- /dev/null
+++ b/lib/gitlab/group_hierarchy.rb
@@ -0,0 +1,104 @@
+module Gitlab
+ # Retrieving of parent or child groups based on a base ActiveRecord relation.
+ #
+ # This class uses recursive CTEs and as a result will only work on PostgreSQL.
+ class GroupHierarchy
+ attr_reader :base, :model
+
+ # base - An instance of ActiveRecord::Relation for which to get parent or
+ # child groups.
+ def initialize(base)
+ @base = base
+ @model = base.model
+ end
+
+ # Returns a relation that includes the base set of groups and all their
+ # ancestors (recursively).
+ def base_and_ancestors
+ return model.none unless Group.supports_nested_groups?
+
+ base_and_ancestors_cte.apply_to(model.all)
+ end
+
+ # Returns a relation that includes the base set of groups and all their
+ # descendants (recursively).
+ def base_and_descendants
+ return model.none unless Group.supports_nested_groups?
+
+ base_and_descendants_cte.apply_to(model.all)
+ end
+
+ # Returns a relation that includes the base groups, their ancestors, and the
+ # descendants of the base groups.
+ #
+ # The resulting query will roughly look like the following:
+ #
+ # WITH RECURSIVE ancestors AS ( ... ),
+ # descendants AS ( ... )
+ # SELECT *
+ # FROM (
+ # SELECT *
+ # FROM ancestors namespaces
+ #
+ # UNION
+ #
+ # SELECT *
+ # FROM descendants namespaces
+ # ) groups;
+ #
+ # Using this approach allows us to further add criteria to the relation with
+ # Rails thinking it's selecting data the usual way.
+ def all_groups
+ return base unless Group.supports_nested_groups?
+
+ ancestors = base_and_ancestors_cte
+ descendants = base_and_descendants_cte
+
+ ancestors_table = ancestors.alias_to(groups_table)
+ descendants_table = descendants.alias_to(groups_table)
+
+ union = SQL::Union.new([model.unscoped.from(ancestors_table),
+ model.unscoped.from(descendants_table)])
+
+ model.
+ unscoped.
+ with.
+ recursive(ancestors.to_arel, descendants.to_arel).
+ from("(#{union.to_sql}) #{model.table_name}")
+ end
+
+ private
+
+ def base_and_ancestors_cte
+ cte = SQL::RecursiveCTE.new(:base_and_ancestors)
+
+ cte << base.except(:order)
+
+ # Recursively get all the ancestors of the base set.
+ cte << model.
+ from([groups_table, cte.table]).
+ where(groups_table[:id].eq(cte.table[:parent_id])).
+ except(:order)
+
+ cte
+ end
+
+ def base_and_descendants_cte
+ cte = SQL::RecursiveCTE.new(:base_and_descendants)
+
+ cte << base.except(:order)
+
+ # Recursively get all the descendants of the base set.
+ cte << model.
+ from([groups_table, cte.table]).
+ where(groups_table[:parent_id].eq(cte.table[:id])).
+ except(:order)
+
+ cte
+ end
+
+ def groups_table
+ model.arel_table
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index df962d203b7..e78b7f22e03 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -2,6 +2,9 @@ module Gitlab
module HealthChecks
class FsShardsCheck
extend BaseAbstractCheck
+ RANDOM_STRING = SecureRandom.hex(1000).freeze
+ COMMAND_TIMEOUT = '1'.freeze
+ TIMEOUT_EXECUTABLE = 'timeout'.freeze
class << self
def readiness
@@ -41,8 +44,6 @@ module Gitlab
private
- RANDOM_STRING = SecureRandom.hex(1000).freeze
-
def operation_metrics(ok_metric, latency_metric, operation, **labels)
with_timing operation do |result, elapsed|
[
@@ -63,8 +64,8 @@ module Gitlab
@storage_paths ||= Gitlab.config.repositories.storages
end
- def with_timeout(args)
- %w{timeout 1}.concat(args)
+ def exec_with_timeout(cmd_args, *args, &block)
+ Gitlab::Popen.popen([TIMEOUT_EXECUTABLE, COMMAND_TIMEOUT].concat(cmd_args), *args, &block)
end
def tmp_file_path(storage_name)
@@ -78,7 +79,7 @@ module Gitlab
def storage_stat_test(storage_name)
stat_path = File.join(path(storage_name), '.')
begin
- _, status = Gitlab::Popen.popen(with_timeout(%W{ stat #{stat_path} }))
+ _, status = exec_with_timeout(%W{ stat #{stat_path} })
status == 0
rescue Errno::ENOENT
File.exist?(stat_path) && File::Stat.new(stat_path).readable?
@@ -86,7 +87,7 @@ module Gitlab
end
def storage_write_test(tmp_path)
- _, status = Gitlab::Popen.popen(with_timeout(%W{ tee #{tmp_path} })) do |stdin|
+ _, status = exec_with_timeout(%W{ tee #{tmp_path} }) do |stdin|
stdin.write(RANDOM_STRING)
end
status == 0
@@ -96,7 +97,7 @@ module Gitlab
end
def storage_read_test(tmp_path)
- _, status = Gitlab::Popen.popen(with_timeout(%W{ diff #{tmp_path} - })) do |stdin|
+ _, status = exec_with_timeout(%W{ diff #{tmp_path} - }) do |stdin|
stdin.write(RANDOM_STRING)
end
status == 0
@@ -106,7 +107,7 @@ module Gitlab
end
def delete_test_file(tmp_path)
- _, status = Gitlab::Popen.popen(with_timeout(%W{ rm -f #{tmp_path} }))
+ _, status = exec_with_timeout(%W{ rm -f #{tmp_path} })
status == 0
rescue Errno::ENOENT
File.delete(tmp_path) rescue Errno::ENOENT
diff --git a/lib/gitlab/project_authorizations/with_nested_groups.rb b/lib/gitlab/project_authorizations/with_nested_groups.rb
new file mode 100644
index 00000000000..bb0df1e3dad
--- /dev/null
+++ b/lib/gitlab/project_authorizations/with_nested_groups.rb
@@ -0,0 +1,125 @@
+module Gitlab
+ module ProjectAuthorizations
+ # Calculating new project authorizations when supporting nested groups.
+ #
+ # This class relies on Common Table Expressions to efficiently get all data,
+ # including data for nested groups. As a result this class can only be used
+ # on PostgreSQL.
+ class WithNestedGroups
+ attr_reader :user
+
+ # user - The User object for which to calculate the authorizations.
+ def initialize(user)
+ @user = user
+ end
+
+ def calculate
+ cte = recursive_cte
+ cte_alias = cte.table.alias(Group.table_name)
+ projects = Project.arel_table
+ links = ProjectGroupLink.arel_table
+
+ relations = [
+ # The project a user has direct access to.
+ user.projects.select_for_project_authorization,
+
+ # The personal projects of the user.
+ user.personal_projects.select_as_master_for_project_authorization,
+
+ # Projects that belong directly to any of the groups the user has
+ # access to.
+ Namespace.
+ unscoped.
+ select([alias_as_column(projects[:id], 'project_id'),
+ cte_alias[:access_level]]).
+ from(cte_alias).
+ joins(:projects),
+
+ # Projects shared with any of the namespaces the user has access to.
+ Namespace.
+ unscoped.
+ select([links[:project_id],
+ least(cte_alias[:access_level],
+ links[:group_access],
+ 'access_level')]).
+ from(cte_alias).
+ joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id').
+ joins('INNER JOIN projects ON projects.id = project_group_links.project_id').
+ joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id').
+ where('p_ns.share_with_group_lock IS FALSE')
+ ]
+
+ union = Gitlab::SQL::Union.new(relations)
+
+ ProjectAuthorization.
+ unscoped.
+ with.
+ recursive(cte.to_arel).
+ select_from_union(union)
+ end
+
+ private
+
+ # Builds a recursive CTE that gets all the groups the current user has
+ # access to, including any nested groups.
+ def recursive_cte
+ cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
+ members = Member.arel_table
+ namespaces = Namespace.arel_table
+
+ # Namespaces the user is a member of.
+ cte << user.groups.
+ select([namespaces[:id], members[:access_level]]).
+ except(:order)
+
+ # Sub groups of any groups the user is a member of.
+ cte << Group.select([namespaces[:id],
+ greatest(members[:access_level],
+ cte.table[:access_level], 'access_level')]).
+ joins(join_cte(cte)).
+ joins(join_members).
+ except(:order)
+
+ cte
+ end
+
+ # Builds a LEFT JOIN to join optional memberships onto the CTE.
+ def join_members
+ members = Member.arel_table
+ namespaces = Namespace.arel_table
+
+ cond = members[:source_id].
+ eq(namespaces[:id]).
+ and(members[:source_type].eq('Namespace')).
+ and(members[:requested_at].eq(nil)).
+ and(members[:user_id].eq(user.id))
+
+ Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
+ end
+
+ # Builds an INNER JOIN to join namespaces onto the CTE.
+ def join_cte(cte)
+ namespaces = Namespace.arel_table
+ cond = cte.table[:id].eq(namespaces[:parent_id])
+
+ Arel::Nodes::InnerJoin.new(cte.table, Arel::Nodes::On.new(cond))
+ end
+
+ def greatest(left, right, column_alias)
+ sql_function('GREATEST', [left, right], column_alias)
+ end
+
+ def least(left, right, column_alias)
+ sql_function('LEAST', [left, right], column_alias)
+ end
+
+ def sql_function(name, args, column_alias)
+ alias_as_column(Arel::Nodes::NamedFunction.new(name, args), column_alias)
+ end
+
+ def alias_as_column(value, alias_to)
+ Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_authorizations/without_nested_groups.rb b/lib/gitlab/project_authorizations/without_nested_groups.rb
new file mode 100644
index 00000000000..627e8c5fba2
--- /dev/null
+++ b/lib/gitlab/project_authorizations/without_nested_groups.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module ProjectAuthorizations
+ # Calculating new project authorizations when not supporting nested groups.
+ class WithoutNestedGroups
+ attr_reader :user
+
+ # user - The User object for which to calculate the authorizations.
+ def initialize(user)
+ @user = user
+ end
+
+ def calculate
+ relations = [
+ # Projects the user is a direct member of
+ user.projects.select_for_project_authorization,
+
+ # Personal projects
+ user.personal_projects.select_as_master_for_project_authorization,
+
+ # Projects of groups the user is a member of
+ user.groups_projects.select_for_project_authorization,
+
+ # Projects shared with groups the user is a member of
+ user.groups.joins(:shared_projects).select_for_project_authorization
+ ]
+
+ union = Gitlab::SQL::Union.new(relations)
+
+ ProjectAuthorization.
+ unscoped.
+ select_from_union(union)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/recursive_cte.rb b/lib/gitlab/sql/recursive_cte.rb
new file mode 100644
index 00000000000..5b1b03820a3
--- /dev/null
+++ b/lib/gitlab/sql/recursive_cte.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module SQL
+ # Class for easily building recursive CTE statements.
+ #
+ # Example:
+ #
+ # cte = RecursiveCTE.new(:my_cte_name)
+ # ns = Arel::Table.new(:namespaces)
+ #
+ # cte << Namespace.
+ # where(ns[:parent_id].eq(some_namespace_id))
+ #
+ # cte << Namespace.
+ # from([ns, cte.table]).
+ # where(ns[:parent_id].eq(cte.table[:id]))
+ #
+ # Namespace.with.
+ # recursive(cte.to_arel).
+ # from(cte.alias_to(ns))
+ class RecursiveCTE
+ attr_reader :table
+
+ # name - The name of the CTE as a String or Symbol.
+ def initialize(name)
+ @table = Arel::Table.new(name)
+ @queries = []
+ end
+
+ # Adds a query to the body of the CTE.
+ #
+ # relation - The relation object to add to the body of the CTE.
+ def <<(relation)
+ @queries << relation
+ end
+
+ # Returns the Arel relation for this CTE.
+ def to_arel
+ sql = Arel::Nodes::SqlLiteral.new(Union.new(@queries).to_sql)
+
+ Arel::Nodes::As.new(table, Arel::Nodes::Grouping.new(sql))
+ end
+
+ # Returns an "AS" statement that aliases the CTE name as the given table
+ # name. This allows one to trick ActiveRecord into thinking it's selecting
+ # from an actual table, when in reality it's selecting from a CTE.
+ #
+ # alias_table - The Arel table to use as the alias.
+ def alias_to(alias_table)
+ Arel::Nodes::As.new(table, alias_table)
+ end
+
+ # Applies the CTE to the given relation, returning a new one that will
+ # query from it.
+ def apply_to(relation)
+ relation.except(:where).
+ with.
+ recursive(to_arel).
+ from(alias_to(relation.model.arel_table))
+ end
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index 9472c3c992f..b341b5a0309 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -87,4 +87,6 @@ shell_path="/bin/bash"
# This variable controls whether the init script starts/stops Gitaly
gitaly_enabled=false
+gitaly_dir=$(cd $app_root/../gitaly 2> /dev/null && pwd)
+gitaly_pid_path="$pid_path/gitaly.pid"
gitaly_log="$app_root/log/gitaly.log"
diff --git a/package.json b/package.json
index 800327d8a08..8e280e77d67 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"jszip-utils": "^0.0.2",
"marked": "^0.3.6",
"mousetrap": "^1.4.6",
+ "name-all-modules-plugin": "^1.0.1",
"pdfjs-dist": "^1.8.252",
"pikaday": "^1.5.1",
"prismjs": "^1.6.0",
@@ -57,8 +58,8 @@
"vue-loader": "^11.3.4",
"vue-resource": "^0.9.3",
"vue-template-compiler": "^2.2.6",
- "webpack": "^2.3.3",
- "webpack-bundle-analyzer": "^2.3.0"
+ "webpack": "^2.6.1",
+ "webpack-bundle-analyzer": "^2.8.2"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 72c82503542..9e2a74ef991 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -1,10 +1,25 @@
FROM ruby:2.3
LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
-RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
- apt-get update && apt-get install -y --force-yes \
- libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
- apt-get clean
+##
+# Update APT sources and install some dependencies
+#
+RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
+RUN apt-get update && apt-get install -y wget git unzip xvfb
+
+##
+# At this point Google Chrome Beta is 59 - first version with headless support
+#
+RUN wget -q https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb
+RUN dpkg -i google-chrome-beta_current_amd64.deb; apt-get -fy install
+
+##
+# Install chromedriver to make it work with Selenium
+#
+RUN wget -q https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip
+RUN unzip chromedriver_linux64.zip -d /usr/local/bin
+
+RUN apt-get clean
WORKDIR /home/qa
diff --git a/qa/Gemfile b/qa/Gemfile
index 6bfe25ba437..5d089a45934 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,6 +2,6 @@ source 'https://rubygems.org'
gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14'
-gem 'capybara-webkit', '~> 1.12.0'
gem 'rake', '~> 12.0.0'
gem 'rspec', '~> 3.5'
+gem 'selenium-webdriver', '~> 2.53'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 6de2abff198..4dd71aa5010 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -16,7 +16,10 @@ GEM
capybara-webkit (1.12.0)
capybara (>= 2.3.0, < 2.13.0)
json
+ childprocess (0.7.0)
+ ffi (~> 1.0, >= 1.0.11)
diff-lcs (1.3)
+ ffi (1.9.18)
json (2.0.3)
launchy (2.4.3)
addressable (~> 2.3)
@@ -44,6 +47,12 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
+ rubyzip (1.2.1)
+ selenium-webdriver (2.53.4)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0)
+ websocket (1.2.4)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -56,6 +65,7 @@ DEPENDENCIES
capybara-webkit (~> 1.12.0)
rake (~> 12.0.0)
rspec (~> 3.5)
+ selenium-webdriver (~> 2.53)
BUNDLED WITH
1.14.6
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index d72187fcd34..78a93828d36 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -1,7 +1,7 @@
require 'rspec/core'
require 'capybara/rspec'
-require 'capybara-webkit'
require 'capybara-screenshot/rspec'
+require 'selenium-webdriver'
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/LineLength
@@ -20,7 +20,6 @@ module QA
configure_rspec!
configure_capybara!
- configure_webkit!
end
def configure_rspec!
@@ -43,9 +42,9 @@ module QA
config.order = :random
Kernel.srand config.seed
- config.before(:all) do
- page.current_window.resize_to(1200, 1800)
- end
+ # config.before(:all) do
+ # page.current_window.resize_to(1200, 1800)
+ # end
config.formatter = :documentation
config.color = true
@@ -53,26 +52,28 @@ module QA
end
def configure_capybara!
+ Capybara.register_driver :chrome do |app|
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ 'chromeOptions' => {
+ 'binary' => '/opt/google/chrome-beta/google-chrome-beta',
+ 'args' => %w[headless no-sandbox disable-gpu]
+ }
+ )
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
+ end
+
Capybara.configure do |config|
config.app_host = @address
- config.default_driver = :webkit
- config.javascript_driver = :webkit
+ config.default_driver = :chrome
+ config.javascript_driver = :chrome
config.default_max_wait_time = 4
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
end
end
-
- def configure_webkit!
- Capybara::Webkit.configure do |config|
- config.allow_url(@address)
- config.block_unknown_urls
- end
- rescue RuntimeError # rubocop:disable Lint/HandleExceptions
- # TODO, Webkit is already configured, this make this
- # configuration step idempotent, should be improved.
- end
end
end
end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index c07a3234673..64d06ef6558 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -12,7 +12,6 @@ RSpec.configure do |config|
config.shared_context_metadata_behavior = :apply_to_host_groups
config.disable_monkey_patching!
config.expose_dsl_globally = true
- config.warnings = true
config.profile_examples = 10
config.order = :random
Kernel.srand config.seed
diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb
new file mode 100644
index 00000000000..3f886cbfea3
--- /dev/null
+++ b/rubocop/cop/migration/update_column_in_batches.rb
@@ -0,0 +1,43 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if a spec file exists for any migration using
+ # `update_column_in_batches`.
+ class UpdateColumnInBatches < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'Migration running `update_column_in_batches` must have a spec file at' \
+ ' `%s`.'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+ return unless node.children[1] == :update_column_in_batches
+
+ spec_path = spec_filename(node)
+
+ unless File.exist?(File.expand_path(spec_path, rails_root))
+ add_offense(node, :expression, format(MSG, spec_path))
+ end
+ end
+
+ private
+
+ def spec_filename(node)
+ source_name = node.location.expression.source_buffer.name
+ path = Pathname.new(source_name).relative_path_from(rails_root)
+ dirname = File.dirname(path)
+ .sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations')
+ filename = File.basename(source_name, '.rb').sub(%r{\A\d+_}, '')
+
+ File.join(dirname, "#{filename}_spec.rb")
+ end
+
+ def rails_root
+ Pathname.new(File.expand_path('../../..', __dir__))
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb
index 3160a784a04..c3473771178 100644
--- a/rubocop/migration_helpers.rb
+++ b/rubocop/migration_helpers.rb
@@ -3,8 +3,9 @@ module RuboCop
module MigrationHelpers
# Returns true if the given node originated from the db/migrate directory.
def in_migration?(node)
- File.dirname(node.location.expression.source_buffer.name).
- end_with?('db/migrate')
+ dirname = File.dirname(node.location.expression.source_buffer.name)
+
+ dirname.end_with?('db/migrate', 'db/post_migrate')
end
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 4ff204f939e..b65efbc41f4 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -8,3 +8,4 @@ require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default'
+require_relative 'cop/migration/update_column_in_batches'
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 565bc314ef1..e4603533872 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -8,7 +8,8 @@ params = {
"ref" => ENV["OMNIBUS_BRANCH"] || "master",
"token" => ENV["BUILD_TRIGGER_TOKEN"],
"variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
- "variables[ALTERNATIVE_SOURCES]" => true
+ "variables[ALTERNATIVE_SOURCES]" => true,
+ "variables[ee]" => ENV["EE_PACKAGE"]
}
Dir.glob("*_VERSION").each do |version_file|
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 7d2f6dd9d0a..2c9d1ffc9c2 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -22,7 +22,7 @@ describe AutocompleteController do
let(:body) { JSON.parse(response.body) }
it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
+ it { expect(body.size).to eq 2 }
it { expect(body.map { |u| u["username"] }).to include(user.username) }
end
@@ -80,8 +80,8 @@ describe AutocompleteController do
end
it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 2 }
- it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+ it { expect(body.size).to eq 3 }
+ it { expect(body.map { |u| u['username'] }).to include(user.username, non_member.username) }
end
end
@@ -97,6 +97,20 @@ describe AutocompleteController do
it { expect(body.size).to eq User.count }
end
+ context 'limited users per page' do
+ let(:per_page) { 2 }
+
+ before do
+ sign_in(user)
+ get(:users, per_page: per_page)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq per_page }
+ end
+
context 'unauthenticated user' do
let(:public_project) { create(:project, :public) }
let(:body) { JSON.parse(response.body) }
@@ -108,7 +122,7 @@ describe AutocompleteController do
end
it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
+ it { expect(body.size).to eq 2 }
end
describe 'GET #users with project' do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 4626f1ebc29..b0b24b1de1b 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -26,7 +26,7 @@ describe GroupsController do
end
end
- describe 'GET #subgroups' do
+ describe 'GET #subgroups', :nested_groups do
let!(:public_subgroup) { create(:group, :public, parent: group) }
let!(:private_subgroup) { create(:group, :private, parent: group) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 587a5820c6f..08024a2148b 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::MergeRequestsController do
let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
@@ -12,7 +12,6 @@ describe Projects::MergeRequestsController do
before do
sign_in(user)
- project.team << [user, :master]
end
describe 'GET new' do
@@ -304,6 +303,8 @@ describe Projects::MergeRequestsController do
end
context 'when user cannot access' do
+ let(:user) { create(:user) }
+
before do
project.add_reporter(user)
xhr :post, :merge, base_params
@@ -459,6 +460,8 @@ describe Projects::MergeRequestsController do
end
describe "DELETE destroy" do
+ let(:user) { create(:user) }
+
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 7a76f5f8afc..e8a9b688319 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -109,6 +109,18 @@ FactoryGirl.define do
merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level
)
+
+ # Normally the class Projects::CreateService is used for creating
+ # projects, and this class takes care of making sure the owner and current
+ # user have access to the project. Our specs don't use said service class,
+ # thus we must manually refresh things here.
+ owner = project.owner
+
+ if owner && owner.is_a?(User) && !project.pending_delete
+ project.members.create!(user: owner, access_level: Gitlab::Access::MASTER)
+ end
+
+ project.group&.refresh_members_authorized_projects
end
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index 1df972843e2..15482347886 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -20,6 +20,7 @@ describe 'Admin System Info' do
expect(page).to have_content 'CPU 2 cores'
expect(page).to have_content 'Memory 4 GB / 16 GB'
expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Uptime'
end
end
@@ -34,6 +35,7 @@ describe 'Admin System Info' do
expect(page).to have_content 'CPU Unable to collect CPU info'
expect(page).to have_content 'Memory 4 GB / 16 GB'
expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Uptime'
end
end
@@ -48,6 +50,7 @@ describe 'Admin System Info' do
expect(page).to have_content 'CPU 2 cores'
expect(page).to have_content 'Memory Unable to collect memory info'
expect(page).to have_content 'Disks'
+ expect(page).to have_content 'Uptime'
end
end
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 01351548a99..fa3435ab719 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -3,10 +3,11 @@ require 'spec_helper'
RSpec.describe 'Dashboard Projects', feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, name: "awesome stuff") }
+ let(:project2) { create(:project, :public, name: 'Community project') }
before do
project.team << [user, :developer]
- login_as user
+ login_as(user)
end
it 'shows the project the user in a member of in the list' do
@@ -14,6 +15,17 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff')
end
+ context 'when on Starred projects tab' do
+ it 'shows only starred projects' do
+ user.toggle_star(project2)
+
+ visit(starred_dashboard_projects_path)
+
+ expect(page).not_to have_content(project.name)
+ expect(page).to have_content(project2.name)
+ end
+ end
+
describe "with a pipeline", redis: true do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
index 8a1d415c4f1..dfc3c84f29a 100644
--- a/spec/features/groups/group_name_toggle_spec.rb
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -22,7 +22,7 @@ feature 'Group name toggle', feature: true, js: true do
expect(page).not_to have_css('.group-name-toggle')
end
- it 'is present if the title is longer than the container' do
+ it 'is present if the title is longer than the container', :nested_groups do
visit group_path(nested_group_3)
title_width = page.evaluate_script("$('.title')[0].offsetWidth")
@@ -35,7 +35,7 @@ feature 'Group name toggle', feature: true, js: true do
expect(title_width).to be > container_width
end
- it 'should show the full group namespace when toggled' do
+ it 'should show the full group namespace when toggled', :nested_groups do
page_height = page.current_window.size[1]
page.current_window.resize_to(SMALL_SCREEN, page_height)
visit group_path(nested_group_3)
diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb
index 543879bd21d..f654fa16a06 100644
--- a/spec/features/groups/members/list_spec.rb
+++ b/spec/features/groups/members/list_spec.rb
@@ -12,7 +12,7 @@ feature 'Groups members list', feature: true do
login_as(user1)
end
- scenario 'show members from current group and parent' do
+ scenario 'show members from current group and parent', :nested_groups do
group.add_developer(user1)
nested_group.add_developer(user2)
@@ -22,7 +22,7 @@ feature 'Groups members list', feature: true do
expect(second_row.text).to include(user2.name)
end
- scenario 'show user once if member of both current group and parent' do
+ scenario 'show user once if member of both current group and parent', :nested_groups do
group.add_developer(user1)
nested_group.add_developer(user1)
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 3d32c47bf09..24ea7aba0cc 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -83,7 +83,7 @@ feature 'Group', feature: true do
end
end
- describe 'create a nested group', js: true do
+ describe 'create a nested group', :nested_groups, js: true do
let(:group) { create(:group, path: 'foo') }
context 'as admin' do
@@ -196,7 +196,7 @@ feature 'Group', feature: true do
end
end
- describe 'group page with nested groups', js: true do
+ describe 'group page with nested groups', :nested_groups, js: true do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:path) { group_path(group) }
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 0b573d7cef4..4d38df05928 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -58,7 +58,7 @@ describe 'Dropdown assignee', :feature, :js do
it 'should load all the assignees when opened' do
filtered_search.set('assignee:')
- expect(dropdown_assignee_size).to eq(3)
+ expect(dropdown_assignee_size).to eq(4)
end
it 'shows current user at top of dropdown' do
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index b29177bed06..358b244fb5b 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -65,7 +65,7 @@ describe 'Dropdown author', js: true, feature: true do
it 'should load all the authors when opened' do
send_keys_to_filtered_search('author:')
- expect(dropdown_author_size).to eq(3)
+ expect(dropdown_author_size).to eq(4)
end
it 'shows current user at top of dropdown' do
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 65d854d0896..009b3bc8bf6 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -219,6 +219,37 @@ describe 'New/edit issue', :feature, :js do
end
end
+ describe 'sub-group project' do
+ let(:group) { create(:group) }
+ let(:nested_group_1) { create(:group, parent: group) }
+ let(:sub_group_project) { create(:empty_project, group: nested_group_1) }
+
+ before do
+ sub_group_project.add_master(user)
+
+ visit new_namespace_project_issue_path(sub_group_project.namespace, sub_group_project)
+ end
+
+ it 'creates new label from dropdown' do
+ click_button 'Labels'
+
+ click_link 'Create new label'
+
+ page.within '.dropdown-new-label' do
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+
+ click_button 'Create'
+
+ wait_for_requests
+ end
+
+ page.within '.dropdown-menu-labels' do
+ expect(page).to have_link 'test label'
+ end
+ end
+ end
+
def before_for_selector(selector)
js = <<-JS.strip_heredoc
(function(selector) {
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index ec87a99b3ab..c77a5c68bc6 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -29,6 +29,19 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
+ it 'allows to unselect "Remove source branch"', js: true do
+ merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
+ expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
+
+ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ uncheck 'Remove source branch when merge request is accepted'
+
+ click_button 'Save changes'
+
+ expect(page).to have_unchecked_field 'remove-source-branch-input'
+ expect(page).to have_content 'Remove source branch'
+ end
+
it 'should preserve description textarea height', js: true do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index e08721b4724..09f889d4dd6 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -7,7 +7,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project,
author: user,
- title: 'Bug NS-04')
+ title: 'Bug NS-04',
+ merge_params: { force_remove_source_branch: '1' })
end
let(:pipeline) do
@@ -41,7 +42,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
click_button "Merge when pipeline succeeds"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will be removed."
+ expect(page).to have_content "The source branch will not be removed."
expect(page).to have_selector ".js-cancel-auto-merge"
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
@@ -82,7 +83,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
source_project: project,
title: 'Bug NS-04',
author: user,
- merge_user: user)
+ merge_user: user,
+ merge_params: { force_remove_source_branch: '1' })
end
before do
@@ -99,7 +101,7 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
click_link 'Merge when pipeline succeeds'
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will be removed."
+ expect(page).to have_content "The source branch will not be removed."
expect(page).to have_link "Cancel automatic merge"
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index be8b1423c20..4f3a5119915 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -202,4 +202,25 @@ describe 'Merge request', :feature, :js do
end
end
end
+
+ context 'user can merge into source project but cannot push to fork', js: true do
+ let(:fork_project) { create(:project, :public) }
+ let(:user2) { create(:user) }
+
+ before do
+ project.team << [user2, :master]
+ logout
+ login_as user2
+ merge_request.update(target_project: fork_project)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'user can merge into the source project' do
+ expect(page).to have_button('Merge', disabled: false)
+ end
+
+ it 'user cannot remove source branch' do
+ expect(page).to have_field('remove-source-branch-input', disabled: true)
+ end
+ end
end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index c969acc9140..4e5682c8636 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -40,7 +40,7 @@ feature 'Project group links', :feature, :js do
another_group.add_master(master)
end
- it 'does not show ancestors' do
+ it 'does not show ancestors', :nested_groups do
visit namespace_project_settings_members_path(project.namespace, project)
click_link 'Search for a group'
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index b7ae5f0b925..d428f6fcf22 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -3,10 +3,9 @@ require 'spec_helper'
feature 'Projects > Members > Sorting', feature: true do
let(:master) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, namespace: master.namespace, creator: master) }
background do
- create(:project_member, :master, user: master, project: project, created_at: 5.days.ago)
create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
login_as(master)
@@ -39,16 +38,16 @@ feature 'Projects > Members > Sorting', feature: true do
scenario 'sorts by last joined' do
visit_members_list(sort: :last_joined)
- expect(first_member).to include(developer.name)
- expect(second_member).to include(master.name)
+ expect(first_member).to include(master.name)
+ expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
scenario 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
- expect(first_member).to include(master.name)
- expect(second_member).to include(developer.name)
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(master.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 1bf8f710b9f..ec48a4bd726 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -2,11 +2,10 @@ require 'spec_helper'
feature 'Projects > Members > User requests access', feature: true do
let(:user) { create(:user) }
- let(:master) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) }
+ let(:master) { project.owner }
background do
- project.team << [master, :master]
login_as(user)
visit namespace_project_path(project.namespace, project)
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index f40e1bc4930..317949d6b56 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -65,6 +65,17 @@ feature 'Pipeline Schedules', :feature do
expect(page).not_to have_content('pipeline schedule')
end
end
+
+ context 'when ref is nil' do
+ before do
+ pipeline_schedule.update_attribute(:ref, nil)
+ visit_pipelines_schedules
+ end
+
+ it 'shows a list of the pipeline schedules with empty ref column' do
+ expect(first('.branch-name-cell').text).to eq('')
+ end
+ end
end
describe 'POST /projects/pipeline_schedules/new', js: true do
@@ -108,6 +119,19 @@ feature 'Pipeline Schedules', :feature do
expect(page).to have_content('my brand new description')
end
+
+ context 'when ref is nil' do
+ before do
+ pipeline_schedule.update_attribute(:ref, nil)
+ edit_pipeline_schedule
+ end
+
+ it 'shows the pipeline schedule with default ref' do
+ page.within('.git-revision-dropdown-toggle') do
+ expect(first('.dropdown-toggle-text').text).to eq('master')
+ end
+ end
+ end
end
def visit_new_pipeline_schedule
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index b762756f9ce..db3fcc23475 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -18,7 +18,7 @@ describe GroupMembersFinder, '#execute' do
expect(result.to_a).to eq([member3, member2, member1])
end
- it 'returns members for nested group' do
+ it 'returns members for nested group', :nested_groups do
group.add_master(user2)
nested_group.request_access(user4)
member1 = group.add_master(user1)
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index cf691cf684b..300ba8422e8 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -9,7 +9,7 @@ describe MembersFinder, '#execute' do
let(:user3) { create(:user) }
let(:user4) { create(:user) }
- it 'returns members for project and parent groups' do
+ it 'returns members for project and parent groups', :nested_groups do
nested_group.request_access(user1)
member1 = group.add_master(user2)
member2 = nested_group.add_master(user3)
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 4afbb87453e..b6a59a6cc47 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -92,7 +92,8 @@
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
"remove_wip_path": { "type": "string" },
- "commits_count": { "type": "integer" }
+ "commits_count": { "type": "integer" },
+ "remove_source_branch": { "type": ["boolean", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 18935be95c9..b05ae5c2232 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -115,6 +115,11 @@ describe SubmoduleHelper do
expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
+ it 'handles urls with trailing whitespace' do
+ stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git ')
+ expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ end
+
it 'returns original with non-standard url' do
stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 90b12c9f115..83c92deccdc 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 } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -45,4 +45,11 @@ describe('Number Utils', () => {
expect(bytesToKiB(1000)).toEqual(0.9765625);
});
});
+
+ describe('bytesToMiB', () => {
+ it('calculates MiB for the given bytes', () => {
+ expect(bytesToMiB(1048576)).toEqual(1);
+ expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
+ });
+ });
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 3d1706aab68..7b910282cc8 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,4 +1,5 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
+/* global Notes */
import '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
@@ -7,6 +8,7 @@ import '~/lib/utils/common_utils';
import '~/diff';
import '~/single_file_diff';
import '~/files_comment_button';
+import '~/notes';
import 'vendor/jquery.scrollTo';
(function () {
@@ -29,7 +31,7 @@ import 'vendor/jquery.scrollTo';
};
$.extend(stubLocation, defaults, stubs || {});
};
- preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw');
beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
@@ -286,8 +288,49 @@ import 'vendor/jquery.scrollTo';
spyOn($, 'ajax').and.callFake(function (options) {
expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
});
+
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
});
+
+ describe('with note fragment hash', () => {
+ beforeEach(() => {
+ loadFixtures('merge_requests/diff_comment.html.raw');
+ spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests');
+ window.notes = new Notes('', []);
+ spyOn(window.notes, 'toggleDiffNote').and.callThrough();
+ });
+
+ afterEach(() => {
+ delete window.notes;
+ });
+
+ it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ const noteId = 'note_1';
+ spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ html: `<div id="${noteId}">foo</div>` });
+ });
+
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+
+ expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'old',
+ forceShow: true,
+ });
+ });
+
+ it('should gracefully ignore non-existant fragment hash', function () {
+ spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ html: '' });
+ });
+
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+
+ expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
+ });
+ });
});
});
}).call(window);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 025f08ee332..04cf0fe2bf8 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -128,7 +128,6 @@ import '~/notes';
beforeEach(() => {
note = {
id: 1,
- discussion_html: null,
valid: true,
note: 'heya',
html: '<div>heya</div>',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index da9dff18ada..2c3d0ddff28 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
const metricsMockData = {
success: true,
metrics: {
+ memory_before: [
+ {
+ metric: {},
+ value: [1495785220.607, '9572875.906976745'],
+ },
+ ],
+ memory_after: [
+ {
+ metric: {},
+ value: [1495787020.607, '4485853.130206379'],
+ },
+ ],
memory_values: [
{
metric: {},
@@ -39,7 +51,7 @@ const createComponent = () => {
const messages = {
loadingMetrics: 'Loading deployment statistics.',
- hasMetrics: 'Deployment memory usage:',
+ hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
loadFailed: 'Failed to load deployment statistics.',
metricsUnavailable: 'Deployment statistics are not available currently.',
};
@@ -89,17 +101,52 @@ describe('MemoryUsage', () => {
});
});
+ describe('computed', () => {
+ describe('memoryChangeType', () => {
+ it('should return "increased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 4.28;
+ vm.memoryTo = 9.13;
+
+ expect(vm.memoryChangeType).toEqual('increased');
+ });
+
+ it('should return "decreased" if memoryFrom value is less than memoryTo value', () => {
+ vm.memoryFrom = 9.13;
+ vm.memoryTo = 4.28;
+
+ expect(vm.memoryChangeType).toEqual('decreased');
+ });
+
+ it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => {
+ vm.memoryFrom = 1;
+ vm.memoryTo = 1;
+
+ expect(vm.memoryChangeType).toEqual('unchanged');
+ });
+ });
+ });
+
describe('methods', () => {
const { metrics, deployment_time } = metricsMockData;
+ describe('getMegabytes', () => {
+ it('should return Megabytes from provided Bytes value', () => {
+ const memoryInBytes = '9572875.906976745';
+
+ expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
+ });
+ });
+
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime } = vm;
+ const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
expect(hasMetrics).toBeTruthy();
expect(memoryMetrics.length > 0).toBeTruthy();
expect(deploymentTime).toEqual(deployment_time);
+ expect(memoryFrom).toEqual('9.13');
+ expect(memoryTo).toEqual('4.28');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index d043ad38b8b..732b516badd 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -5,7 +5,7 @@ import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
-const createComponent = () => {
+const createComponent = (customConfig = {}) => {
const Component = Vue.extend(readyToMergeComponent);
const mr = {
isPipelineActive: false,
@@ -17,8 +17,12 @@ const createComponent = () => {
sha: '12345678',
commitMessage,
commitMessageWithDescription,
+ shouldRemoveSourceBranch: true,
+ canRemoveSourceBranch: false,
};
+ Object.assign(mr, customConfig.mr);
+
const service = {
merge() {},
poll() {},
@@ -51,7 +55,6 @@ describe('MRWidgetReadyToMerge', () => {
describe('data', () => {
it('should have default data', () => {
- expect(vm.removeSourceBranch).toBeTruthy(true);
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
@@ -166,6 +169,36 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
});
+
+ describe('Remove source branch checkbox', () => {
+ describe('when user can merge but cannot delete branch', () => {
+ it('isRemoveSourceBranchButtonDisabled should be true', () => {
+ expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true);
+ });
+
+ it('should be disabled in the rendered output', () => {
+ const checkboxElement = vm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('when user can merge and can delete branch', () => {
+ beforeEach(() => {
+ this.customVm = createComponent({
+ mr: { canRemoveSourceBranch: true },
+ });
+ });
+
+ it('isRemoveSourceBranchButtonDisabled should be false', () => {
+ expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ });
+
+ it('should be enabled in rendered output', () => {
+ const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ expect(checkboxElement.getAttribute('disabled')).toBeNull();
+ });
+ });
+ });
});
describe('methods', () => {
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
new file mode 100644
index 00000000000..1bf8916b3d0
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
+
+describe('Header CI Component', () => {
+ let HeaderCi;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderCi = Vue.extend(headerCi);
+
+ props = {
+ status: {
+ group: 'failed',
+ icon: 'ci-status-failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ actions: [
+ {
+ label: 'Retry',
+ path: 'path',
+ type: 'button',
+ cssClass: 'btn',
+ },
+ {
+ label: 'Go',
+ path: 'path',
+ type: 'link',
+ cssClass: 'link',
+ },
+ ],
+ };
+
+ vm = new HeaderCi({
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(
+ vm.$el.querySelector('.ci-failed').getAttribute('href'),
+ ).toEqual(props.status.details_path);
+ });
+
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
+
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
+
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ });
+
+ it('should render provided actions', () => {
+ expect(vm.$el.querySelector('.btn').tagName).toEqual('BUTTON');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toEqual(props.actions[0].label);
+ expect(vm.$el.querySelector('.link').tagName).toEqual('A');
+ expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
+ expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
new file mode 100644
index 00000000000..bf28019ef24
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import '~/lib/utils/datetime_utility';
+
+describe('Time ago with tooltip component', () => {
+ let TimeagoTooltip;
+ let vm;
+
+ beforeEach(() => {
+ TimeagoTooltip = Vue.extend(timeagoTooltip);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render timeago with a bootstrap tooltip', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ },
+ }).$mount();
+
+ expect(vm.$el.tagName).toEqual('TIME');
+ expect(vm.$el.classList.contains('js-timeago')).toEqual(true);
+ expect(
+ vm.$el.getAttribute('data-original-title'),
+ ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
+ expect(vm.$el.getAttribute('data-placement')).toEqual('top');
+
+ const timeago = gl.utils.getTimeago();
+
+ expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z'));
+ });
+
+ it('should render tooltip placed in bottom', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ tooltipPlacement: 'bottom',
+ },
+ }).$mount();
+
+ expect(vm.$el.getAttribute('data-placement')).toEqual('bottom');
+ });
+
+ it('should render short format class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ shortFormat: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true);
+ });
+
+ it('should render provided html class', () => {
+ vm = new TimeagoTooltip({
+ propsData: {
+ time: '2017-05-08T14:57:39.781Z',
+ cssClass: 'foo',
+ },
+ }).$mount();
+
+ expect(vm.$el.classList.contains('foo')).toEqual(true);
+ });
+});
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index b386852b196..cfb5cba054e 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do
- let(:project) { create(:project) }
+ let!(:project) { create(:project) }
let(:pipeline_status) { described_class.new(project) }
let(:cache_key) { "projects/#{project.id}/pipeline_status" }
@@ -18,7 +18,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do
let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
let(:ref) { 'master' }
let(:pipeline_info) { { sha: sha, status: status, ref: ref } }
- let(:project_without_status) { create(:project) }
+ let!(:project_without_status) { create(:project) }
describe '.load_in_batch_for_projects' do
it 'preloads pipeline_status on projects' do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index 40ac5a3ed37..bbb3f9912a3 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -240,9 +240,50 @@ describe Gitlab::Ci::Trace::Stream do
end
context 'multiple results in content & regex' do
- let(:data) { ' (98.39%) covered. (98.29%) covered' }
+ let(:data) do
+ <<~HEREDOC
+ (98.39%) covered
+ (98.29%) covered
+ HEREDOC
+ end
+
+ let(:regex) { '\(\d+.\d+\%\) covered' }
+
+ it 'returns the last matched coverage' do
+ is_expected.to eq("98.29")
+ end
+ end
+
+ context 'when BUFFER_SIZE is smaller than stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
+
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq("98.29") }
+ end
+
+ context 'when regex is multi-byte char' do
+ let(:data) { '95.0 ゴッドファット\n' }
+ let(:regex) { '\d+\.\d+ ゴッドファット' }
+
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq('95.0') }
+ end
+
+ context 'when BUFFER_SIZE is equal to stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
let(:regex) { '\(\d+.\d+\%\) covered' }
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
+ end
+
it { is_expected.to eq("98.29") }
end
diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
index b5f2c387d6f..8c979ae1869 100644
--- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -24,12 +24,16 @@ describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do
"url": "https://github.com/vuejs/vue.git"
},
"homepage": "https://github.com/vuejs/vue#readme",
+ "scripts": {
+ "karma": "karma start config/karma.config.js --single-run"
+ },
"dependencies": {
"primus": "*",
"async": "~0.8.0",
"express": "4.2.x",
"bigpipe": "bigpipe/pagelet",
- "plates": "https://github.com/flatiron/plates/tarball/master"
+ "plates": "https://github.com/flatiron/plates/tarball/master",
+ "karma": "^1.4.1"
},
"devDependencies": {
"vows": "^0.7.0",
@@ -69,6 +73,7 @@ describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do
expect(subject).to include(link('express', 'https://npmjs.com/package/express'))
expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe'))
expect(subject).to include(link('plates', 'https://npmjs.com/package/plates'))
+ expect(subject).to include(link('karma', 'https://npmjs.com/package/karma'))
expect(subject).to include(link('vows', 'https://npmjs.com/package/vows'))
expect(subject).to include(link('assume', 'https://npmjs.com/package/assume'))
expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit'))
@@ -81,5 +86,9 @@ describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do
it 'links Git repos' do
expect(subject).to include(link('https://github.com/flatiron/plates/tarball/master', 'https://github.com/flatiron/plates/tarball/master'))
end
+
+ it 'does not link scripts with the same key as a package' do
+ expect(subject).not_to include(link('karma start config/karma.config.js --single-run', 'https://github.com/karma start config/karma.config.js --single-run'))
+ end
end
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 122c93dcd69..ae617b313c5 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
no_collapse: no_collapse
)
end
- let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) }
+ let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) }
let(:file_count) { 0 }
let(:line_length) { 1 }
let(:line_count) { 1 }
@@ -64,7 +64,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
subject { super().real_size }
it { is_expected.to eq('3') }
end
- it { expect(subject.size).to eq(3) }
+
+ describe '#size' do
+ it { expect(subject.size).to eq(3) }
+
+ it 'does not change after peeking' do
+ subject.any?
+ expect(subject.size).to eq(3)
+ end
+ end
context 'when limiting is disabled' do
let(:all_diffs) { true }
@@ -83,7 +91,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
subject { super().real_size }
it { is_expected.to eq('3') }
end
- it { expect(subject.size).to eq(3) }
+
+ describe '#size' do
+ it { expect(subject.size).to eq(3) }
+
+ it 'does not change after peeking' do
+ subject.any?
+ expect(subject.size).to eq(3)
+ end
+ end
end
end
@@ -457,4 +473,22 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
def fake_diff(line_length, line_count)
{ 'diff' => "#{'a' * line_length}\n" * line_count }
end
+
+ class MutatingConstantIterator
+ include Enumerable
+
+ def initialize(count, value)
+ @count = count
+ @value = value
+ end
+
+ def each
+ loop do
+ break if @count.zero?
+ # It is critical to decrement before yielding. We may never reach the lines after 'yield'.
+ @count -= 1
+ yield @value
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
new file mode 100644
index 00000000000..5d0ed1522b3
--- /dev/null
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Gitlab::GroupHierarchy, :postgresql do
+ let!(:parent) { create(:group) }
+ let!(:child1) { create(:group, parent: parent) }
+ let!(:child2) { create(:group, parent: child1) }
+
+ describe '#base_and_ancestors' do
+ let(:relation) do
+ described_class.new(Group.where(id: child2.id)).base_and_ancestors
+ end
+
+ it 'includes the base rows' do
+ expect(relation).to include(child2)
+ end
+
+ it 'includes all of the ancestors' do
+ expect(relation).to include(parent, child1)
+ end
+ end
+
+ describe '#base_and_descendants' do
+ let(:relation) do
+ described_class.new(Group.where(id: parent.id)).base_and_descendants
+ end
+
+ it 'includes the base rows' do
+ expect(relation).to include(parent)
+ end
+
+ it 'includes all the descendants' do
+ expect(relation).to include(child1, child2)
+ end
+ end
+
+ describe '#all_groups' do
+ let(:relation) do
+ described_class.new(Group.where(id: child1.id)).all_groups
+ end
+
+ it 'includes the base rows' do
+ expect(relation).to include(child1)
+ end
+
+ it 'includes the ancestors' do
+ expect(relation).to include(parent)
+ end
+
+ it 'includes the descendants' do
+ expect(relation).to include(child2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 45ccd3d6459..61c10d47434 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -1,6 +1,24 @@
require 'spec_helper'
describe Gitlab::HealthChecks::FsShardsCheck do
+ def command_exists?(command)
+ _, status = Gitlab::Popen.popen(%W{ #{command} 1 echo })
+ status == 0
+ rescue Errno::ENOENT
+ false
+ end
+
+ def timeout_command
+ @timeout_command ||=
+ if command_exists?('timeout')
+ 'timeout'
+ elsif command_exists?('gtimeout')
+ 'gtimeout'
+ else
+ ''
+ end
+ end
+
let(:metric_class) { Gitlab::HealthChecks::Metric }
let(:result_class) { Gitlab::HealthChecks::Result }
let(:repository_storages) { [:default] }
@@ -15,6 +33,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
before do
allow(described_class).to receive(:repository_storages) { repository_storages }
allow(described_class).to receive(:storages_paths) { storages_paths }
+ stub_const('Gitlab::HealthChecks::FsShardsCheck::TIMEOUT_EXECUTABLE', timeout_command)
end
after do
@@ -78,40 +97,76 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access
end
- it { is_expected.to include(metric_class.new(:filesystem_accessible, 0, shard: :default)) }
- it { is_expected.to include(metric_class.new(:filesystem_readable, 0, shard: :default)) }
- it { is_expected.to include(metric_class.new(:filesystem_writable, 0, shard: :default)) }
+ it { is_expected.to all(have_attributes(labels: { shard: :default })) }
+
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) }
- it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) }
end
context 'storage points to directory that has both read and write rights' do
before do
FileUtils.chmod_R(0755, tmp_dir)
end
+ it { is_expected.to all(have_attributes(labels: { shard: :default })) }
- it { is_expected.to include(metric_class.new(:filesystem_accessible, 1, shard: :default)) }
- it { is_expected.to include(metric_class.new(:filesystem_readable, 1, shard: :default)) }
- it { is_expected.to include(metric_class.new(:filesystem_writable, 1, shard: :default)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) }
- it { is_expected.to include(have_attributes(name: :filesystem_access_latency, value: be >= 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_read_latency, value: be >= 0, labels: { shard: :default })) }
- it { is_expected.to include(have_attributes(name: :filesystem_write_latency, value: be >= 0, labels: { shard: :default })) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) }
+ it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) }
+ end
+ end
+ end
+
+ context 'when timeout kills fs checks' do
+ before do
+ stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '1')
+
+ allow(described_class).to receive(:exec_with_timeout).and_wrap_original { |m| m.call(%w(sleep 60)) }
+ FileUtils.chmod_R(0755, tmp_dir)
+ end
+
+ describe '#readiness' do
+ subject { described_class.readiness }
+
+ it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
+ end
+
+ describe '#metrics' do
+ subject { described_class.metrics }
+
+ it 'provides metrics' do
+ expect(subject).to all(have_attributes(labels: { shard: :default }))
+
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
+
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0))
+ expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0))
end
end
end
context 'when popen always finds required binaries' do
before do
- allow(Gitlab::Popen).to receive(:popen).and_wrap_original do |method, *args, &block|
+ allow(described_class).to receive(:exec_with_timeout).and_wrap_original do |method, *args, &block|
begin
method.call(*args, &block)
- rescue RuntimeError
+ rescue RuntimeError, Errno::ENOENT
raise 'expected not to happen'
end
end
+
+ stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '10')
end
it_behaves_like 'filesystem checks'
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index b9d4e59e770..3e0291c9ae9 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ImportExport::MembersMapper, services: true do
describe 'map members' do
- let(:user) { create(:admin, authorized_projects_populated: true) }
+ let(:user) { create(:admin) }
let(:project) { create(:empty_project, :public, name: 'searchable_project') }
- let(:user2) { create(:user, authorized_projects_populated: true) }
+ let(:user2) { create(:user) }
let(:exported_user_id) { 99 }
let(:exported_members) do
[{
@@ -74,7 +74,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
end
context 'user is not an admin' do
- let(:user) { create(:user, authorized_projects_populated: true) }
+ let(:user) { create(:user) }
it 'does not map a project member' do
expect(members_mapper.map[exported_user_id]).to eq(user.id)
@@ -94,7 +94,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
end
context 'importer same as group member' do
- let(:user2) { create(:admin, authorized_projects_populated: true) }
+ let(:user2) { create(:admin) }
let(:group) { create(:group) }
let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) }
let(:members_mapper) do
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
new file mode 100644
index 00000000000..67321f43710
--- /dev/null
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::ProjectAuthorizations do
+ let(:group) { create(:group) }
+ let!(:owned_project) { create(:empty_project) }
+ let!(:other_project) { create(:empty_project) }
+ let!(:group_project) { create(:empty_project, namespace: group) }
+
+ let(:user) { owned_project.namespace.owner }
+
+ def map_access_levels(rows)
+ rows.each_with_object({}) do |row, hash|
+ hash[row.project_id] = row.access_level
+ end
+ end
+
+ before do
+ other_project.team << [user, :reporter]
+ group.add_developer(user)
+ end
+
+ let(:authorizations) do
+ klass = if Group.supports_nested_groups?
+ Gitlab::ProjectAuthorizations::WithNestedGroups
+ else
+ Gitlab::ProjectAuthorizations::WithoutNestedGroups
+ end
+
+ klass.new(user).calculate
+ end
+
+ it 'returns the correct number of authorizations' do
+ expect(authorizations.length).to eq(3)
+ end
+
+ it 'includes the correct projects' do
+ expect(authorizations.pluck(:project_id)).
+ to include(owned_project.id, other_project.id, group_project.id)
+ end
+
+ it 'includes the correct access levels' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[owned_project.id]).to eq(Gitlab::Access::MASTER)
+ expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
+ expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+
+ if Group.supports_nested_groups?
+ context 'with nested groups' do
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:nested_project) { create(:empty_project, namespace: nested_group) }
+
+ it 'includes nested groups' do
+ expect(authorizations.pluck(:project_id)).to include(nested_project.id)
+ end
+
+ it 'inherits access levels when the user is not a member of a nested group' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[nested_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'uses the greatest access level when a user is a member of a nested group' do
+ nested_group.add_master(user)
+
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[nested_project.id]).to eq(Gitlab::Access::MASTER)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 1b8690ba613..3d22784909d 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -123,8 +123,8 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'when wiki is internal' do
let(:project) { create(:project, :public, :wiki_private) }
- it 'finds wiki blobs for members' do
- project.add_reporter(user)
+ it 'finds wiki blobs for guest' do
+ project.add_guest(user)
is_expected.not_to be_empty
end
diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb
new file mode 100644
index 00000000000..25146860615
--- /dev/null
+++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::SQL::RecursiveCTE, :postgresql do
+ let(:cte) { described_class.new(:cte_name) }
+
+ describe '#to_arel' do
+ it 'generates an Arel relation for the CTE body' do
+ rel1 = User.where(id: 1)
+ rel2 = User.where(id: 2)
+
+ cte << rel1
+ cte << rel2
+
+ sql = cte.to_arel.to_sql
+ name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+
+ sql1, sql2 = ActiveRecord::Base.connection.unprepared_statement do
+ [rel1.except(:order).to_sql, rel2.except(:order).to_sql]
+ end
+
+ expect(sql).to eq("#{name} AS (#{sql1}\nUNION\n#{sql2})")
+ end
+ end
+
+ describe '#alias_to' do
+ it 'returns an alias for the CTE' do
+ table = Arel::Table.new(:kittens)
+
+ source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
+ alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens)
+
+ expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
+ end
+ end
+
+ describe '#apply_to' do
+ it 'applies a CTE to an ActiveRecord::Relation' do
+ user = create(:user)
+ cte = described_class.new(:cte_name)
+
+ cte << User.where(id: user.id)
+
+ relation = cte.apply_to(User.all)
+
+ expect(relation.to_sql).to match(/WITH RECURSIVE.+cte_name/)
+ expect(relation.to_a).to eq(User.where(id: user.id).to_a)
+ end
+ end
+end
diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb
deleted file mode 100644
index 99dc4195818..00000000000
--- a/spec/migrations/fill_authorized_projects_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb')
-
-describe FillAuthorizedProjects do
- describe '#up' do
- it 'schedules the jobs in batches' do
- user1 = create(:user)
- user2 = create(:user)
-
- expect(Sidekiq::Client).to receive(:push_bulk).with(
- 'class' => 'AuthorizedProjectsWorker',
- 'args' => [[user1.id], [user2.id]]
- )
-
- described_class.new.up
- end
- end
-end
diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb
new file mode 100644
index 00000000000..175bf1876b2
--- /dev/null
+++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb')
+
+describe TurnNestedGroupsIntoRegularGroupsForMysql do
+ let!(:parent_group) { create(:group) }
+ let!(:child_group) { create(:group, parent: parent_group) }
+ let!(:project) { create(:project, :empty_repo, namespace: child_group) }
+ let!(:member) { create(:user) }
+ let(:migration) { described_class.new }
+
+ before do
+ parent_group.add_developer(member)
+
+ allow(migration).to receive(:run_migration?).and_return(true)
+ allow(migration).to receive(:verbose).and_return(false)
+ end
+
+ describe '#up' do
+ let(:updated_project) do
+ # path_with_namespace is memoized in an instance variable so we retrieve a
+ # new row here to work around that.
+ Project.find(project.id)
+ end
+
+ before do
+ migration.up
+ end
+
+ it 'unsets the parent_id column' do
+ expect(Namespace.where('parent_id IS NOT NULL').any?).to eq(false)
+ end
+
+ it 'adds members of parent groups as members to the migrated group' do
+ is_member = child_group.members.
+ where(user_id: member, access_level: Gitlab::Access::DEVELOPER).any?
+
+ expect(is_member).to eq(true)
+ end
+
+ it 'update the path of the nested group' do
+ child_group.reload
+
+ expect(child_group.path).to eq("#{parent_group.name}-#{child_group.name}")
+ end
+
+ it 'renames projects of the nested group' do
+ expect(updated_project.path_with_namespace).
+ to eq("#{parent_group.name}-#{child_group.name}/#{updated_project.path}")
+ end
+
+ it 'renames the repository of any projects' do
+ expect(updated_project.repository.path).
+ to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git")
+
+ expect(File.directory?(updated_project.repository.path)).to eq(true)
+ end
+
+ it 'creates a redirect route for renamed projects' do
+ exists = RedirectRoute.
+ where(source_type: 'Project', source_id: project.id).
+ any?
+
+ expect(exists).to eq(true)
+ end
+ end
+end
diff --git a/spec/migrations/update_retried_for_ci_builds_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb
index 3742b4dafe5..3742b4dafe5 100644
--- a/spec/migrations/update_retried_for_ci_builds_spec.rb
+++ b/spec/migrations/update_retried_for_ci_build_spec.rb
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 822b98c5f6c..b00e7a73571 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -25,6 +25,14 @@ describe Ci::PipelineSchedule, models: true do
expect(pipeline_schedule).not_to be_valid
end
+
+ context 'when active is false' do
+ it 'does not allow nullified ref' do
+ pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil)
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+ end
end
describe '#set_next_run_at' do
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 49a4132f763..0e10d91836d 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -115,123 +115,6 @@ describe Group, 'Routable' do
end
end
- describe '.member_descendants' do
- let!(:user) { create(:user) }
- let!(:nested_group) { create(:group, parent: group) }
-
- before { group.add_owner(user) }
- subject { described_class.member_descendants(user.id) }
-
- it { is_expected.to eq([nested_group]) }
- end
-
- describe '.member_self_and_descendants' do
- let!(:user) { create(:user) }
- let!(:nested_group) { create(:group, parent: group) }
-
- before { group.add_owner(user) }
- subject { described_class.member_self_and_descendants(user.id) }
-
- it { is_expected.to match_array [group, nested_group] }
- end
-
- describe '.member_hierarchy' do
- # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
- let!(:user) { create(:user) }
-
- # group
- # _______ (foo) _______
- # | |
- # | |
- # nested_group_1 nested_group_2
- # (bar) (barbaz)
- # | |
- # | |
- # nested_group_1_1 nested_group_2_1
- # (baz) (baz)
- #
- let!(:nested_group_1) { create :group, parent: group, name: 'bar' }
- let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' }
- let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' }
- let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' }
-
- context 'user is not a member of any group' do
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns an empty array' do
- is_expected.to eq []
- end
- end
-
- context 'user is member of all groups' do
- before do
- group.add_owner(user)
- nested_group_1.add_owner(user)
- nested_group_1_1.add_owner(user)
- nested_group_2.add_owner(user)
- nested_group_2_1.add_owner(user)
- end
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns all groups' do
- is_expected.to match_array [
- group,
- nested_group_1, nested_group_1_1,
- nested_group_2, nested_group_2_1
- ]
- end
- end
-
- context 'user is member of the top group' do
- before { group.add_owner(user) }
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns all groups' do
- is_expected.to match_array [
- group,
- nested_group_1, nested_group_1_1,
- nested_group_2, nested_group_2_1
- ]
- end
- end
-
- context 'user is member of the first child (internal node), branch 1' do
- before { nested_group_1.add_owner(user) }
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns the groups in the hierarchy' do
- is_expected.to match_array [
- group,
- nested_group_1, nested_group_1_1
- ]
- end
- end
-
- context 'user is member of the first child (internal node), branch 2' do
- before { nested_group_2.add_owner(user) }
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns the groups in the hierarchy' do
- is_expected.to match_array [
- group,
- nested_group_2, nested_group_2_1
- ]
- end
- end
-
- context 'user is member of the last child (leaf node)' do
- before { nested_group_1_1.add_owner(user) }
- subject { described_class.member_hierarchy(user.id) }
-
- it 'returns the groups in the hierarchy' do
- is_expected.to match_array [
- group,
- nested_group_1, nested_group_1_1
- ]
- end
- end
- end
-
describe '#full_path' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6ca1eb0374d..316bf153660 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -340,7 +340,7 @@ describe Group, models: true do
it { expect(subject.parent).to be_kind_of(Group) }
end
- describe '#members_with_parents' do
+ describe '#members_with_parents', :nested_groups do
let!(:group) { create(:group, :nested) }
let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 87ea2e70680..cf9c701e8c5 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -22,16 +22,15 @@ describe ProjectMember, models: true do
end
describe '.add_user' do
- context 'when called with the project owner' do
- it 'adds the user as a member' do
- project = create(:empty_project)
+ it 'adds the user as a member' do
+ user = create(:user)
+ project = create(:empty_project)
- expect(project.users).not_to include(project.owner)
+ expect(project.users).not_to include(user)
- described_class.add_user(project, project.owner, :master, current_user: project.owner)
+ described_class.add_user(project, user, :master, current_user: project.owner)
- expect(project.users.reload).to include(project.owner)
- end
+ expect(project.users.reload).to include(user)
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index e3e8e6d571c..aa1ce89ffd7 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -249,4 +249,17 @@ describe Milestone, models: true do
expect(milestone.to_reference(another_project)).to eq "sample-project%1"
end
end
+
+ describe '#participants' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:milestone) { build(:milestone, iid: 1, project: project) }
+
+ it 'returns participants without duplicates' do
+ user = create :user
+ create :issue, project: project, milestone: milestone, assignees: [user]
+ create :issue, project: project, milestone: milestone, assignees: [user]
+
+ expect(milestone.participants).to eq [user]
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ff5e7c350aa..0e74f1ab1bd 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -287,21 +287,21 @@ describe Namespace, models: true do
end
end
- describe '#ancestors' do
+ describe '#ancestors', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct ancestors' do
- expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group])
- expect(deep_nested_group.ancestors).to eq([group, nested_group])
- expect(nested_group.ancestors).to eq([group])
+ expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group)
+ expect(deep_nested_group.ancestors).to include(group, nested_group)
+ expect(nested_group.ancestors).to include(group)
expect(group.ancestors).to eq([])
end
end
- describe '#descendants' do
+ describe '#descendants', :nested_groups do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
@@ -311,9 +311,9 @@ describe Namespace, models: true do
it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([])
- expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group])
- expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group])
- expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
+ expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group)
+ expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
+ expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 9b711bfc007..4161b9158b1 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -23,7 +23,7 @@ describe ProjectGroupLink do
expect(project_group_link).not_to be_valid
end
- it "doesn't allow a project to be shared with an ancestor of the group it is in" do
+ it "doesn't allow a project to be shared with an ancestor of the group it is in", :nested_groups do
project_group_link.group = parent_group
expect(project_group_link).not_to be_valid
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 4bca0229e7a..349067e73ab 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -22,6 +22,42 @@ describe JiraService, models: true do
it { is_expected.not_to validate_presence_of(:url) }
end
+
+ context 'validating urls' do
+ let(:service) do
+ described_class.new(
+ project: create(:empty_project),
+ active: true,
+ username: 'username',
+ password: 'test',
+ project_key: 'TEST',
+ jira_issue_transition_id: 24,
+ url: 'http://jira.test.com'
+ )
+ end
+
+ it 'is valid when all fields have required values' do
+ expect(service).to be_valid
+ end
+
+ it 'is not valid when url is not a valid url' do
+ service.url = 'not valid'
+
+ expect(service).not_to be_valid
+ end
+
+ it 'is not valid when api url is not a valid url' do
+ service.api_url = 'not valid'
+
+ expect(service).not_to be_valid
+ end
+
+ it 'is valid when api url is a valid url' do
+ service.api_url = 'http://jira.test.com/api'
+
+ expect(service).to be_valid
+ end
+ end
end
describe '#reference_pattern' do
@@ -187,22 +223,29 @@ describe JiraService, models: true do
describe '#test_settings' do
let(:jira_service) do
described_class.new(
+ project: create(:project),
url: 'http://jira.example.com',
- username: 'gitlab_jira_username',
- password: 'gitlab_jira_password',
+ username: 'jira_username',
+ password: 'jira_password',
project_key: 'GitLabProject'
)
end
- let(:project_url) { 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' }
- before do
+ def test_settings(api_url)
+ project_url = "http://jira_username:jira_password@#{api_url}/rest/api/2/project/GitLabProject"
+
WebMock.stub_request(:get, project_url)
- end
- it 'tries to get JIRA project' do
jira_service.test_settings
+ end
+
+ it 'tries to get JIRA project with URL when API URL not set' do
+ test_settings('jira.example.com')
+ end
- expect(WebMock).to have_requested(:get, project_url)
+ it 'tries to get JIRA project with API URL if set' do
+ jira_service.update(api_url: 'http://jira.api.com')
+ test_settings('jira.api.com')
end
end
@@ -214,34 +257,75 @@ describe JiraService, models: true do
@jira_service = JiraService.create!(
project: project,
properties: {
- url: 'http://jira.example.com/rest/api/2',
+ url: 'http://jira.example.com/web',
username: 'mic',
password: "password"
}
)
end
- it "reset password if url changed" do
- @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
- @jira_service.save
- expect(@jira_service.password).to be_nil
+ context 'when only web url present' do
+ it 'reset password if url changed' do
+ @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+
+ it 'reset password if url not changed but api url added' do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
end
- it "does not reset password if username changed" do
- @jira_service.username = "some_name"
+ context 'when both web and api url present' do
+ before do
+ @jira_service.api_url = 'http://jira.example.com/rest/api/2'
+ @jira_service.password = 'password'
+
+ @jira_service.save
+ end
+ it 'reset password if api url changed' do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+
+ it 'does not reset password if url changed' do
+ @jira_service.url = 'http://jira_edited.example.com/rweb'
+ @jira_service.save
+
+ expect(@jira_service.password).to eq("password")
+ end
+
+ it 'reset password if api url set to ""' do
+ @jira_service.api_url = ''
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+ end
+
+ it 'does not reset password if username changed' do
+ @jira_service.username = 'some_name'
@jira_service.save
- expect(@jira_service.password).to eq("password")
+
+ expect(@jira_service.password).to eq('password')
end
- it "does not reset password if new url is set together with password, even if it's the same password" do
+ it 'does not reset password if new url is set together with password, even if it\'s the same password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
- expect(@jira_service.password).to eq("password")
- expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
+
+ expect(@jira_service.password).to eq('password')
+ expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
- it "resets password if url changed, even if setter called multiple times" do
+ it 'resets password if url changed, even if setter called multiple times' do
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
@@ -249,7 +333,7 @@ describe JiraService, models: true do
end
end
- context "when no password was previously set" do
+ context 'when no password was previously set' do
before do
@jira_service = JiraService.create(
project: project,
@@ -260,26 +344,16 @@ describe JiraService, models: true do
)
end
- it "saves password if new url is set together with password" do
+ it 'saves password if new url is set together with password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
- expect(@jira_service.password).to eq("password")
- expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
+ expect(@jira_service.password).to eq('password')
+ expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
end
end
- describe "Validations" do
- context "active" do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of :url }
- end
- end
-
describe 'description and title' do
let(:project) { create(:empty_project) }
@@ -321,9 +395,10 @@ describe JiraService, models: true do
context 'when gitlab.yml was initialized' do
before do
settings = {
- "jira" => {
- "title" => "Jira",
- "url" => "http://jira.sample/projects/project_a"
+ 'jira' => {
+ 'title' => 'Jira',
+ 'url' => 'http://jira.sample/projects/project_a',
+ 'api_url' => 'http://jira.sample/api'
}
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
@@ -335,8 +410,9 @@ describe JiraService, models: true do
end
it 'is prepopulated with the settings' do
- expect(@service.properties["title"]).to eq('Jira')
- expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties['title']).to eq('Jira')
+ expect(@service.properties['url']).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties['api_url']).to eq('http://jira.sample/api')
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f2b4e9070b4..36575acf671 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1431,6 +1431,31 @@ describe Project, models: true do
end
end
+ describe 'Project import job' do
+ let(:project) { create(:empty_project) }
+ let(:mirror) { false }
+
+ before do
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
+ .with(project.repository_storage_path, project.path_with_namespace, project.import_url)
+ .and_return(true)
+
+ allow(project).to receive(:repository_exists?).and_return(true)
+
+ expect_any_instance_of(Repository).to receive(:after_import)
+ .and_call_original
+ end
+
+ it 'imports a project' do
+ expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
+
+ project.import_start
+ project.add_import_job
+
+ expect(project.reload.import_status).to eq('finished')
+ end
+ end
+
describe '#latest_successful_builds_for' do
def create_pipeline(status = 'success')
create(:ci_pipeline, project: project,
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 942eeab251d..fb2d5f60009 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -81,7 +81,7 @@ describe ProjectTeam, models: true do
user = create(:user)
project.add_guest(user)
- expect(project.team.members).to contain_exactly(user)
+ expect(project.team.members).to contain_exactly(user, project.owner)
end
it 'returns project members of a specified level' do
@@ -100,7 +100,8 @@ describe ProjectTeam, models: true do
group_access: Gitlab::Access::GUEST
)
- expect(project.team.members).to contain_exactly(group_member.user)
+ expect(project.team.members).
+ to contain_exactly(group_member.user, project.owner)
end
it 'returns invited members of a group of a specified level' do
@@ -137,7 +138,10 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable)
+ end
+
let(:requester) { create(:user) }
before do
@@ -200,7 +204,9 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable)
+ end
context 'when project is not shared with group' do
before do
@@ -244,7 +250,9 @@ describe ProjectTeam, models: true do
context 'group project' do
let(:group) { create(:group, :access_requestable) }
- let!(:project) { create(:empty_project, group: group) }
+ let!(:project) do
+ create(:empty_project, group: group)
+ end
before do
group.add_master(master)
@@ -265,8 +273,15 @@ describe ProjectTeam, models: true do
let(:group) { create(:group) }
let(:developer) { create(:user) }
let(:master) { create(:user) }
- let(:personal_project) { create(:empty_project, namespace: developer.namespace) }
- let(:group_project) { create(:empty_project, namespace: group) }
+
+ let(:personal_project) do
+ create(:empty_project, namespace: developer.namespace)
+ end
+
+ let(:group_project) do
+ create(:empty_project, namespace: group)
+ end
+
let(:members_project) { create(:empty_project) }
let(:shared_project) { create(:empty_project) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index aabdac4bb75..9edf34b05ad 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -627,16 +627,6 @@ describe User, models: true do
it { expect(User.without_projects).to include user_without_project2 }
end
- describe '.not_in_project' do
- before do
- User.delete_all
- @user = create :user
- @project = create(:empty_project)
- end
-
- it { expect(User.not_in_project(@project)).to include(@user, @project.owner) }
- end
-
describe 'user creation' do
describe 'normal user' do
let(:user) { create(:user, name: 'John Smith') }
@@ -1561,48 +1551,103 @@ describe User, models: true do
end
end
- describe '#nested_groups' do
+ describe '#all_expanded_groups' do
+ # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, parent: group) }
- before do
- group.add_owner(user)
+ # group
+ # _______ (foo) _______
+ # | |
+ # | |
+ # nested_group_1 nested_group_2
+ # (bar) (barbaz)
+ # | |
+ # | |
+ # nested_group_1_1 nested_group_2_1
+ # (baz) (baz)
+ #
+ let!(:group) { create :group }
+ let!(:nested_group_1) { create :group, parent: group, name: 'bar' }
+ let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' }
+ let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' }
+ let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' }
- # Add more data to ensure method does not include wrong groups
- create(:group).add_owner(create(:user))
+ subject { user.all_expanded_groups }
+
+ context 'user is not a member of any group' do
+ it 'returns an empty array' do
+ is_expected.to eq([])
+ end
end
- it { expect(user.nested_groups).to eq([nested_group]) }
- end
+ context 'user is member of all groups' do
+ before do
+ group.add_owner(user)
+ nested_group_1.add_owner(user)
+ nested_group_1_1.add_owner(user)
+ nested_group_2.add_owner(user)
+ nested_group_2_1.add_owner(user)
+ end
- describe '#all_expanded_groups' do
- let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:nested_group_1) { create(:group, parent: group) }
- let!(:nested_group_2) { create(:group, parent: group) }
+ it 'returns all groups' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1,
+ nested_group_2, nested_group_2_1
+ ]
+ end
+ end
- before { nested_group_1.add_owner(user) }
+ context 'user is member of the top group' do
+ before { group.add_owner(user) }
- it { expect(user.all_expanded_groups).to match_array [group, nested_group_1] }
- end
+ if Group.supports_nested_groups?
+ it 'returns all groups' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1,
+ nested_group_2, nested_group_2_1
+ ]
+ end
+ else
+ it 'returns the top-level groups' do
+ is_expected.to match_array [group]
+ end
+ end
+ end
- describe '#nested_groups_projects' do
- let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:project) { create(:empty_project, namespace: group) }
- let!(:nested_project) { create(:empty_project, namespace: nested_group) }
+ context 'user is member of the first child (internal node), branch 1', :nested_groups do
+ before { nested_group_1.add_owner(user) }
- before do
- group.add_owner(user)
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1
+ ]
+ end
+ end
+
+ context 'user is member of the first child (internal node), branch 2', :nested_groups do
+ before { nested_group_2.add_owner(user) }
- # Add more data to ensure method does not include wrong projects
- other_project = create(:empty_project, namespace: create(:group, :nested))
- other_project.add_developer(create(:user))
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_2, nested_group_2_1
+ ]
+ end
end
- it { expect(user.nested_groups_projects).to eq([nested_project]) }
+ context 'user is member of the last child (leaf node)', :nested_groups do
+ before { nested_group_1_1.add_owner(user) }
+
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1
+ ]
+ end
+ end
end
describe '#refresh_authorized_projects', redis: true do
@@ -1622,10 +1667,6 @@ describe User, models: true do
expect(user.project_authorizations.count).to eq(2)
end
- it 'sets the authorized_projects_populated column' do
- expect(user.authorized_projects_populated).to eq(true)
- end
-
it 'stores the correct access levels' do
expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true)
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
@@ -1735,7 +1776,7 @@ describe User, models: true do
end
end
- context 'with 2FA requirement on nested parent group' do
+ context 'with 2FA requirement on nested parent group', :nested_groups do
let!(:group1) { create :group, require_two_factor_authentication: true }
let!(:group1a) { create :group, require_two_factor_authentication: false, parent: group1 }
@@ -1750,7 +1791,7 @@ describe User, models: true do
end
end
- context 'with 2FA requirement on nested child group' do
+ context 'with 2FA requirement on nested child group', :nested_groups do
let!(:group1) { create :group, require_two_factor_authentication: false }
let!(:group1a) { create :group, require_two_factor_authentication: true, parent: group1 }
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 2077c14ff7a..4c37a553227 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -107,7 +107,7 @@ describe GroupPolicy, models: true do
end
end
- describe 'private nested group inherit permissions' do
+ describe 'private nested group inherit permissions', :nested_groups do
let(:nested_group) { create(:group, :private, parent: group) }
subject { described_class.abilities(current_user, nested_group).to_set }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 0b0e4c2b112..b84361d3abd 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,6 @@ describe API::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 90b36374ded..bb53796cbd7 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -429,7 +429,7 @@ describe API::Groups do
expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end
- it "creates a nested group" do
+ it "creates a nested group", :nested_groups do
parent = create(:group)
parent.add_owner(user3)
group = attributes_for(:group, { parent_id: parent.id })
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index f9e5316b3de..9e6957e9922 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -7,7 +7,7 @@ describe API::Pipelines do
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch)
+ ref: project.default_branch, user: user)
end
before { project.team << [user, :master] }
@@ -232,20 +232,26 @@ describe API::Pipelines do
context 'when order_by and sort are specified' do
context 'when order_by user_id' do
- let!(:pipeline) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) }
+ before do
+ 3.times do
+ create(:ci_pipeline, project: project, user: create(:user))
+ end
+ end
- it 'sorts as user_id: :asc' do
- get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'asc'
+ context 'when sort parameter is valid' do
+ it 'sorts as user_id: :desc' do
+ get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'desc'
- expect(response).to have_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).not_to be_empty
- pipeline.sort_by { |p| p.user.id }.tap do |sorted_pipeline|
- json_response.each_with_index { |r, i| expect(r['id']).to eq(sorted_pipeline[i].id) }
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).not_to be_empty
+
+ pipeline_ids = Ci::Pipeline.all.order(user_id: :desc).pluck(:id)
+ expect(json_response.map { |r| r['id'] }).to eq(pipeline_ids)
end
end
- context 'when sort is invalid' do
+ context 'when sort parameter is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'invalid_sort'
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index d5c3b5b34ad..f95a287a184 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -11,8 +11,7 @@ describe API::Projects do
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
- let(:project_member) { create(:project_member, :master, user: user, project: project) }
- let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
+ let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
create(:project,
@@ -27,7 +26,7 @@ describe API::Projects do
builds_enabled: false,
snippets_enabled: false)
end
- let(:project_member3) do
+ let(:project_member2) do
create(:project_member,
user: user4,
project: project3,
@@ -210,7 +209,7 @@ describe API::Projects do
let(:public_project) { create(:empty_project, :public) }
before do
- project_member2
+ project_member
user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
@@ -784,19 +783,18 @@ describe API::Projects do
describe 'GET /projects/:id/users' do
shared_examples_for 'project users response' do
it 'returns the project users' do
- member = create(:user)
- create(:project_member, :developer, user: member, project: project)
-
get api("/projects/#{project.id}/users", current_user)
+ user = project.namespace.owner
+
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
first_user = json_response.first
- expect(first_user['username']).to eq(member.username)
- expect(first_user['name']).to eq(member.name)
+ expect(first_user['username']).to eq(user.username)
+ expect(first_user['name']).to eq(user.name)
expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
end
end
@@ -1091,8 +1089,8 @@ describe API::Projects do
before { user4 }
before { project3 }
before { project4 }
- before { project_member3 }
before { project_member2 }
+ before { project_member }
it 'returns 400 when nothing sent' do
project_param = {}
@@ -1573,7 +1571,7 @@ describe API::Projects do
context 'when authenticated as developer' do
before do
- project_member2
+ project_member
end
it 'returns forbidden error' do
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index c2e8c3ae6f7..386f60065ad 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -5,7 +5,6 @@ describe API::V3::Commits do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index bc261b5e07c..98e8c954909 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -421,7 +421,7 @@ describe API::V3::Groups do
expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end
- it "creates a nested group" do
+ it "creates a nested group", :nested_groups do
parent = create(:group)
parent.add_owner(user3)
group = attributes_for(:group, { parent_id: parent.id })
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index dc7c3d125b1..bc591b2eb37 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -10,8 +10,7 @@ describe API::V3::Projects do
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
- let(:project_member) { create(:project_member, :master, user: user, project: project) }
- let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
+ let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
create(:project,
@@ -25,7 +24,7 @@ describe API::V3::Projects do
issues_enabled: false, wiki_enabled: false,
snippets_enabled: false)
end
- let(:project_member3) do
+ let(:project_member2) do
create(:project_member,
user: user4,
project: project3,
@@ -286,7 +285,7 @@ describe API::V3::Projects do
let(:public_project) { create(:empty_project, :public) }
before do
- project_member2
+ project_member
user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
@@ -622,7 +621,6 @@ describe API::V3::Projects do
context 'when authenticated' do
before do
project
- project_member
end
it 'returns a project by id' do
@@ -814,8 +812,7 @@ describe API::V3::Projects do
describe 'GET /projects/:id/users' do
shared_examples_for 'project users response' do
it 'returns the project users' do
- member = create(:user)
- create(:project_member, :developer, user: member, project: project)
+ member = project.owner
get v3_api("/projects/#{project.id}/users", current_user)
@@ -1163,8 +1160,8 @@ describe API::V3::Projects do
before { user4 }
before { project3 }
before { project4 }
- before { project_member3 }
before { project_member2 }
+ before { project_member }
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
new file mode 100644
index 00000000000..968dcd6232e
--- /dev/null
+++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/migration/update_column_in_batches'
+
+describe RuboCop::Cop::Migration::UpdateColumnInBatches do
+ let(:cop) { described_class.new }
+ let(:tmp_rails_root) { Rails.root.join('tmp', 'rails_root') }
+ let(:migration_code) do
+ <<-END
+ def up
+ update_column_in_batches(:projects, :name, "foo") do |table, query|
+ query.where(table[:name].eq(nil))
+ end
+ end
+ END
+ end
+
+ before do
+ allow(cop).to receive(:rails_root).and_return(tmp_rails_root)
+ end
+ after do
+ FileUtils.rm_rf(tmp_rails_root)
+ end
+
+ context 'outside of a migration' do
+ it 'does not register any offenses' do
+ inspect_source(cop, migration_code)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ let(:spec_filepath) { tmp_rails_root.join('spec', 'migrations', 'my_super_migration_spec.rb') }
+
+ shared_context 'with a migration file' do
+ before do
+ FileUtils.mkdir_p(File.dirname(migration_filepath))
+ @migration_file = File.new(migration_filepath, 'w+')
+ end
+ after do
+ @migration_file.close
+ end
+ end
+
+ shared_examples 'a migration file with no spec file' do
+ include_context 'with a migration file'
+
+ let(:relative_spec_filepath) { Pathname.new(spec_filepath).relative_path_from(tmp_rails_root) }
+
+ it 'registers an offense when using update_column_in_batches' do
+ inspect_source(cop, migration_code, @migration_file)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([2])
+ expect(cop.offenses.first.message).
+ to include("`#{relative_spec_filepath}`")
+ end
+ end
+ end
+
+ shared_examples 'a migration file with a spec file' do
+ include_context 'with a migration file'
+
+ before do
+ FileUtils.mkdir_p(File.dirname(spec_filepath))
+ @spec_file = File.new(spec_filepath, 'w+')
+ end
+ after do
+ @spec_file.close
+ end
+
+ it 'does not register any offenses' do
+ inspect_source(cop, migration_code, @migration_file)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'in a migration' do
+ let(:migration_filepath) { tmp_rails_root.join('db', 'migrate', '20121220064453_my_super_migration.rb') }
+
+ it_behaves_like 'a migration file with no spec file'
+ it_behaves_like 'a migration file with a spec file'
+ end
+
+ context 'in a post migration' do
+ let(:migration_filepath) { tmp_rails_root.join('db', 'post_migrate', '20121220064453_my_super_migration.rb') }
+
+ it_behaves_like 'a migration file with no spec file'
+ it_behaves_like 'a migration file with a spec file'
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 0a1f41719f7..be0e829880e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -41,6 +41,12 @@ describe Issues::CloseService, services: true do
service.execute(issue)
end
+
+ it 'invalidates counter cache for assignees' do
+ expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+
+ service.execute(issue)
+ end
end
describe '#close_issue' do
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 93a8270fd16..391ecad303a 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -27,6 +27,13 @@ describe Issues::ReopenService, services: true do
project.team << [user, :master]
end
+ it 'invalidates counter cache for assignees' do
+ issue.assignees << user
+ expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+
+ described_class.new(project, user).execute(issue)
+ end
+
context 'when issue is not confidential' do
it 'executes issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index d55a7657c0e..154f30aac3b 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -15,6 +15,8 @@ describe MergeRequests::CloseService, services: true do
end
describe '#execute' do
+ it_behaves_like 'cache counters invalidator'
+
context 'valid params' do
let(:service) { described_class.new(project, user, {}) }
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
new file mode 100644
index 00000000000..a20b32eda5f
--- /dev/null
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe MergeRequests::PostMergeService, services: true do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:project) { merge_request.project }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe '#execute' do
+ it_behaves_like 'cache counters invalidator'
+ end
+end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index a99d4eac9bd..b6d4db2f922 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -14,6 +14,8 @@ describe MergeRequests::ReopenService, services: true do
end
describe '#execute' do
+ it_behaves_like 'cache counters invalidator'
+
context 'valid params' do
let(:service) { described_class.new(project, user, {}) }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 4b8589b2736..0d6dd28e332 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -70,7 +70,7 @@ describe Projects::DestroyService, services: true do
end
end
- expect(project.team.members.count).to eq 1
+ expect(project.team.members.count).to eq 2
end
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 2112f1cf9ea..5cf989105d0 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -26,6 +26,15 @@ describe SearchService, services: true do
expect(project).to eq accessible_project
end
+
+ it 'returns the project for guests' do
+ search_project = create :empty_project
+ search_project.add_guest(user)
+
+ project = SearchService.new(user, project_id: search_project.id).project
+
+ expect(project).to eq search_project
+ end
end
context 'when the project is not accessible' do
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index b19374ef1a2..8c40d25e00c 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -1,15 +1,13 @@
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
- let(:project) { create(:empty_project) }
+ # We're using let! here so that any expectations for the service class are not
+ # triggered twice.
+ let!(:project) { create(:empty_project) }
+
let(:user) { project.namespace.owner }
let(:service) { described_class.new(user) }
- def create_authorization(project, user, access_level = Gitlab::Access::MASTER)
- ProjectAuthorization.
- create!(project: project, user: user, access_level: access_level)
- end
-
describe '#execute', :redis do
it 'refreshes the authorizations using a lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
@@ -31,7 +29,8 @@ describe Users::RefreshAuthorizedProjectsService do
it 'updates the authorized projects of the user' do
project2 = create(:empty_project)
- to_remove = create_authorization(project2, user)
+ to_remove = user.project_authorizations.
+ create!(project: project2, access_level: Gitlab::Access::MASTER)
expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
@@ -40,7 +39,10 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'sets the access level of a project to the highest available level' do
- to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
+ user.project_authorizations.delete_all
+
+ to_remove = user.project_authorizations.
+ create!(project: project, access_level: Gitlab::Access::DEVELOPER)
expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
@@ -61,34 +63,10 @@ describe Users::RefreshAuthorizedProjectsService do
service.update_authorizations([], [])
end
-
- context 'when the authorized projects column is not set' do
- before do
- user.update!(authorized_projects_populated: nil)
- end
-
- it 'populates the authorized projects column' do
- service.update_authorizations([], [])
-
- expect(user.authorized_projects_populated).to eq true
- end
- end
-
- context 'when the authorized projects column is set' do
- before do
- user.update!(authorized_projects_populated: true)
- end
-
- it 'does nothing' do
- expect(user).not_to receive(:set_authorized_projects_column)
-
- service.update_authorizations([], [])
- end
- end
end
it 'removes authorizations that should be removed' do
- authorization = create_authorization(project, user)
+ authorization = user.project_authorizations.find_by(project_id: project.id)
service.update_authorizations([authorization.project_id])
@@ -96,6 +74,8 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'inserts authorizations that should be added' do
+ user.project_authorizations.delete_all
+
service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
authorizations = user.project_authorizations
@@ -105,16 +85,6 @@ describe Users::RefreshAuthorizedProjectsService do
expect(authorizations[0].project_id).to eq(project.id)
expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER)
end
-
- it 'populates the authorized projects column' do
- # make sure we start with a nil value no matter what the default in the
- # factory may be.
- user.update!(authorized_projects_populated: nil)
-
- service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
-
- expect(user.authorized_projects_populated).to eq(true)
- end
end
describe '#fresh_access_levels_per_project' do
@@ -163,7 +133,7 @@ describe Users::RefreshAuthorizedProjectsService do
end
end
- context 'projects of subgroups of groups the user is a member of' do
+ context 'projects of subgroups of groups the user is a member of', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let!(:other_project) { create(:empty_project, group: nested_group) }
@@ -191,7 +161,7 @@ describe Users::RefreshAuthorizedProjectsService do
end
end
- context 'projects shared with subgroups of groups the user is a member of' do
+ context 'projects shared with subgroups of groups the user is a member of', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:other_project) { create(:empty_project) }
@@ -208,8 +178,6 @@ describe Users::RefreshAuthorizedProjectsService do
end
describe '#current_authorizations_per_project' do
- before { create_authorization(project, user) }
-
let(:hash) { service.current_authorizations_per_project }
it 'returns a Hash' do
@@ -233,13 +201,13 @@ describe Users::RefreshAuthorizedProjectsService do
describe '#current_authorizations' do
context 'without authorizations' do
it 'returns an empty list' do
+ user.project_authorizations.delete_all
+
expect(service.current_authorizations.empty?).to eq(true)
end
end
context 'with an authorization' do
- before { create_authorization(project, user) }
-
let(:row) { service.current_authorizations.take }
it 'returns the currently authorized projects' do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 51571ddebe9..4c2eba8fa46 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -92,6 +92,14 @@ RSpec.configure do |config|
Gitlab::Redis.with(&:flushall)
Sidekiq.redis(&:flushall)
end
+
+ config.around(:each, :nested_groups) do |example|
+ example.run if Group.supports_nested_groups?
+ end
+
+ config.around(:each, :postgresql) do |example|
+ example.run if Gitlab::Database.postgresql?
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/issuable_shared_examples.rb
new file mode 100644
index 00000000000..03011535351
--- /dev/null
+++ b/spec/support/issuable_shared_examples.rb
@@ -0,0 +1,7 @@
+shared_examples 'cache counters invalidator' do
+ it 'invalidates counter cache for assignees' do
+ expect_any_instance_of(User).to receive(:invalidate_merge_request_cache_counts)
+
+ described_class.new(project, user, {}).execute(merge_request)
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 8aac2b1b1cd..8221711960d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25,7 +25,7 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn@4.0.4:
+acorn@4.0.4, acorn@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
@@ -33,9 +33,9 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.11, acorn@^4.0.3, acorn@^4.0.4:
- version "4.0.11"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+acorn@^5.0.0, acorn@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d"
after@0.8.2:
version "0.8.2"
@@ -1560,6 +1560,12 @@ debug@2.6.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0:
dependencies:
ms "0.7.2"
+debug@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
+ dependencies:
+ ms "2.0.0"
+
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1612,7 +1618,7 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-depd@~1.1.0:
+depd@1.1.0, depd@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
@@ -1733,7 +1739,7 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-ejs@^2.5.5:
+ejs@^2.5.6:
version "2.5.6"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
@@ -2081,9 +2087,9 @@ esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
-etag@~1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8"
+etag@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
eve-raphael@0.5.0:
version "0.5.0"
@@ -2166,9 +2172,9 @@ exports-loader@^0.6.4:
loader-utils "^1.0.2"
source-map "0.5.x"
-express@^4.13.3, express@^4.14.1:
- version "4.14.1"
- resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33"
+express@^4.13.3, express@^4.15.2:
+ version "4.15.3"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
dependencies:
accepts "~1.3.3"
array-flatten "1.1.1"
@@ -2176,26 +2182,28 @@ express@^4.13.3, express@^4.14.1:
content-type "~1.0.2"
cookie "0.3.1"
cookie-signature "1.0.6"
- debug "~2.2.0"
+ debug "2.6.7"
depd "~1.1.0"
encodeurl "~1.0.1"
escape-html "~1.0.3"
- etag "~1.7.0"
- finalhandler "0.5.1"
- fresh "0.3.0"
+ etag "~1.8.0"
+ finalhandler "~1.0.3"
+ fresh "0.5.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.1"
path-to-regexp "0.1.7"
- proxy-addr "~1.1.3"
- qs "6.2.0"
+ proxy-addr "~1.1.4"
+ qs "6.4.0"
range-parser "~1.2.0"
- send "0.14.2"
- serve-static "~1.11.2"
- type-is "~1.6.14"
+ send "0.15.3"
+ serve-static "1.12.3"
+ setprototypeof "1.0.3"
+ statuses "~1.3.1"
+ type-is "~1.6.15"
utils-merge "1.0.0"
- vary "~1.1.0"
+ vary "~1.1.1"
extend@^3.0.0, extend@~3.0.0:
version "3.0.0"
@@ -2287,9 +2295,9 @@ filesize@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122"
-filesize@^3.5.4:
- version "3.5.4"
- resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.4.tgz#742fc7fb6aef4ee3878682600c22f840731e1fda"
+filesize@^3.5.9:
+ version "3.5.10"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f"
fill-range@^2.1.0:
version "2.2.3"
@@ -2311,13 +2319,15 @@ finalhandler@0.5.0:
statuses "~1.3.0"
unpipe "~1.0.0"
-finalhandler@0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd"
+finalhandler@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89"
dependencies:
- debug "~2.2.0"
+ debug "2.6.7"
+ encodeurl "~1.0.1"
escape-html "~1.0.3"
on-finished "~2.3.0"
+ parseurl "~1.3.1"
statuses "~1.3.1"
unpipe "~1.0.0"
@@ -2385,9 +2395,9 @@ forwarded@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
-fresh@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
+fresh@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
from@~0:
version "0.1.7"
@@ -2689,6 +2699,15 @@ http-errors@~1.5.0, http-errors@~1.5.1:
setprototypeof "1.0.2"
statuses ">= 1.3.1 < 2"
+http-errors@~1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
+ dependencies:
+ depd "1.1.0"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
http-proxy-middleware@~0.17.4:
version "0.17.4"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
@@ -2808,9 +2827,9 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
-ipaddr.js@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4"
+ipaddr.js@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
is-absolute-url@^2.0.0:
version "2.1.0"
@@ -3198,7 +3217,7 @@ json3@3.3.2, json3@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
-json5@^0.5.0:
+json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
@@ -3643,7 +3662,17 @@ miller-rabin@^4.0.0:
version "1.26.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff"
-mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
+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.15, mime-types@~2.1.7:
+ version "2.1.15"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
+ dependencies:
+ mime-db "~1.27.0"
+
+mime-types@~2.1.11:
version "2.1.14"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
dependencies:
@@ -3699,10 +3728,18 @@ ms@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
mute-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+name-all-modules-plugin@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c"
+
nan@^2.0.0, nan@^2.3.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
@@ -3931,7 +3968,7 @@ onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-opener@^1.4.2:
+opener@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
@@ -4485,12 +4522,12 @@ proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
-proxy-addr@~1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074"
+proxy-addr@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
dependencies:
forwarded "~0.1.0"
- ipaddr.js "1.2.0"
+ ipaddr.js "1.3.0"
prr@~0.0.0:
version "0.0.0"
@@ -4532,14 +4569,14 @@ qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
-qs@6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
-
qs@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
+qs@6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
@@ -4913,7 +4950,7 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
-safe-buffer@^5.0.1:
+safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -4947,20 +4984,20 @@ semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
-send@0.14.2:
- version "0.14.2"
- resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef"
+send@0.15.3:
+ version "0.15.3"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309"
dependencies:
- debug "~2.2.0"
+ debug "2.6.7"
depd "~1.1.0"
destroy "~1.0.4"
encodeurl "~1.0.1"
escape-html "~1.0.3"
- etag "~1.7.0"
- fresh "0.3.0"
- http-errors "~1.5.1"
+ etag "~1.8.0"
+ fresh "0.5.0"
+ http-errors "~1.6.1"
mime "1.3.4"
- ms "0.7.2"
+ ms "2.0.0"
on-finished "~2.3.0"
range-parser "~1.2.0"
statuses "~1.3.1"
@@ -4977,14 +5014,14 @@ serve-index@^1.7.2:
mime-types "~2.1.11"
parseurl "~1.3.1"
-serve-static@~1.11.2:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7"
+serve-static@1.12.3:
+ version "1.12.3"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2"
dependencies:
encodeurl "~1.0.1"
escape-html "~1.0.3"
parseurl "~1.3.1"
- send "0.14.2"
+ send "0.15.3"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
@@ -5002,6 +5039,10 @@ setprototypeof@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
sha.js@^2.3.6:
version "2.4.8"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f"
@@ -5496,20 +5537,20 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
-type-is@~1.6.14:
- version "1.6.14"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2"
+type-is@~1.6.14, type-is@~1.6.15:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
dependencies:
media-typer "0.3.0"
- mime-types "~2.1.13"
+ mime-types "~2.1.15"
typedarray@^0.0.6, typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.6, uglify-js@^2.8.5:
- version "2.8.21"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.21.tgz#1733f669ae6f82fc90c7b25ec0f5c783ee375314"
+uglify-js@^2.6, uglify-js@^2.8.27:
+ version "2.8.27"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c"
dependencies:
source-map "~0.5.1"
yargs "~3.10.0"
@@ -5528,6 +5569,10 @@ ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+ultron@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
+
unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@@ -5640,9 +5685,9 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0"
-vary@~1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140"
+vary@~1.1.0, vary@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
vendors@^1.0.0:
version "1.0.1"
@@ -5729,20 +5774,21 @@ wbuf@^1.1.0, wbuf@^1.4.0:
dependencies:
minimalistic-assert "^1.0.0"
-webpack-bundle-analyzer@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.3.0.tgz#0d05e96a43033f7cc57f6855b725782ba61e93a4"
+webpack-bundle-analyzer@^2.8.2:
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.8.2.tgz#8b6240c29a9d63bc72f09d920fb050adbcce9fe8"
dependencies:
- acorn "^4.0.11"
+ acorn "^5.0.3"
chalk "^1.1.3"
commander "^2.9.0"
- ejs "^2.5.5"
- express "^4.14.1"
- filesize "^3.5.4"
+ ejs "^2.5.6"
+ express "^4.15.2"
+ filesize "^3.5.9"
gzip-size "^3.0.0"
lodash "^4.17.4"
mkdirp "^0.5.1"
- opener "^1.4.2"
+ opener "^1.4.3"
+ ws "^2.3.1"
webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
version "1.10.0"
@@ -5789,11 +5835,11 @@ webpack-sources@^0.2.3:
source-list-map "^1.1.1"
source-map "~0.5.3"
-webpack@^2.3.3:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.3.3.tgz#eecc083c18fb7bf958ea4f40b57a6640c5a0cc78"
+webpack@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.6.1.tgz#2e0457f0abb1ac5df3ab106c69c672f236785f07"
dependencies:
- acorn "^4.0.4"
+ acorn "^5.0.0"
acorn-dynamic-import "^2.0.0"
ajv "^4.7.0"
ajv-keywords "^1.1.1"
@@ -5801,6 +5847,7 @@ webpack@^2.3.3:
enhanced-resolve "^3.0.0"
interpret "^1.0.0"
json-loader "^0.5.4"
+ json5 "^0.5.1"
loader-runner "^2.3.0"
loader-utils "^0.2.16"
memory-fs "~0.4.1"
@@ -5809,7 +5856,7 @@ webpack@^2.3.3:
source-map "^0.5.3"
supports-color "^3.1.0"
tapable "~0.2.5"
- uglify-js "^2.8.5"
+ uglify-js "^2.8.27"
watchpack "^1.3.1"
webpack-sources "^0.2.3"
yargs "^6.0.0"
@@ -5898,6 +5945,13 @@ ws@1.1.1:
options ">=0.0.5"
ultron "1.0.x"
+ws@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80"
+ dependencies:
+ safe-buffer "~5.0.1"
+ ultron "~1.1.0"
+
wtf-8@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"