summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin (godfat) <godfat@godfat.org>2017-05-26 19:18:07 +0000
committerLin Jen-Shin (godfat) <godfat@godfat.org>2017-05-26 19:18:07 +0000
commitb5e8de5c4c202d10ef8b48248b12f49c1330d04a (patch)
treee034bee79723e84eab1e9257c3848d4c991d5333
parent8f44bc4dc10caf3c9856a8e4bea5ac145a315131 (diff)
parent3605e4307ddf373071c0353a066c50fcead01e45 (diff)
downloadgitlab-ce-b5e8de5c4c202d10ef8b48248b12f49c1330d04a.tar.gz
Merge branch 'master' into '24196-protected-variables'
# Conflicts: # db/schema.rb
-rw-r--r--.gitlab-ci.yml20
-rw-r--r--.scss-lint.yml66
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/raven/raven_config.js3
-rw-r--r--app/assets/javascripts/single_file_diff.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/awards.scss7
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss4
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-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.scss83
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/controllers/admin/hook_logs_controller.rb29
-rw-r--r--app/controllers/admin/hooks_controller.rb32
-rw-r--r--app/controllers/application_controller.rb38
-rw-r--r--app/controllers/concerns/diff_for_path.rb13
-rw-r--r--app/controllers/concerns/hooks_execution.rb15
-rw-r--r--app/controllers/dashboard/projects_controller.rb3
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb8
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/hook_logs_controller.rb33
-rw-r--r--app/controllers/projects/hooks_controller.rb17
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/helpers/commits_helper.rb10
-rw-r--r--app/helpers/diff_helper.rb18
-rw-r--r--app/helpers/labels_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/rss_helper.rb2
-rw-r--r--app/mailers/base_mailer.rb6
-rw-r--r--app/models/concerns/note_on_diff.rb10
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/system_hook.rb4
-rw-r--r--app/models/hooks/web_hook.rb43
-rw-r--r--app/models/hooks/web_hook_log.rb13
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/merge_request.rb34
-rw-r--r--app/models/merge_request_diff.rb23
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/jira_service.rb37
-rw-r--r--app/models/project_services/kubernetes_service.rb24
-rw-r--r--app/models/user.rb14
-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/web_hook_service.rb120
-rw-r--r--app/validators/dynamic_path_validator.rb22
-rw-r--r--app/views/admin/hook_logs/_index.html.haml37
-rw-r--r--app/views/admin/hook_logs/show.html.haml10
-rw-r--r--app/views/admin/hooks/edit.html.haml6
-rw-r--r--app/views/admin/requests_profiles/index.html.haml2
-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/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-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.html.haml1
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/notify/repository_push_email.html.haml28
-rw-r--r--app/views/notify/repository_push_email.text.haml20
-rw-r--r--app/views/profiles/accounts/_reset_token.html.haml11
-rw-r--r--app/views/profiles/accounts/show.html.haml34
-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/diffs/_content.html.haml21
-rw-r--r--app/views/projects/diffs/_diffs.html.haml10
-rw-r--r--app/views/projects/diffs/_file.html.haml12
-rw-r--r--app/views/projects/diffs/_file_header.html.haml16
-rw-r--r--app/views/projects/diffs/_image.html.haml69
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml6
-rw-r--r--app/views/projects/diffs/_text_file.html.haml4
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml68
-rw-r--r--app/views/projects/diffs/viewers/_text.html.haml8
-rw-r--r--app/views/projects/hook_logs/_index.html.haml37
-rw-r--r--app/views/projects/hook_logs/show.html.haml11
-rw-r--r--app/views/projects/hooks/edit.html.haml8
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/merge_requests/index.html.haml4
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/hook_logs/_content.html.haml44
-rw-r--r--app/views/shared/hook_logs/_status_label.html.haml3
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml23
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/process_commit_worker.rb15
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb10
-rw-r--r--app/workers/system_hook_worker.rb10
-rw-r--r--app/workers/web_hook_worker.rb (renamed from app/workers/project_web_hook_worker.rb)6
-rw-r--r--changelogs/unreleased/12614-fix-long-message-from-mr.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/32799-remove-no_turbolink-attribute-from-haml.yml4
-rw-r--r--changelogs/unreleased/32807-company-icon.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/dm-more-dependency-linkers.yml4
-rw-r--r--changelogs/unreleased/feature-rss-scoped-token.yml4
-rw-r--r--changelogs/unreleased/fix-counter-cache-for-acts-as-taggable.yml4
-rw-r--r--changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml4
-rw-r--r--changelogs/unreleased/issuable-form-create-label-sub-groups.yml4
-rw-r--r--changelogs/unreleased/issue_19262.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/0_acts_as_taggable.rb (renamed from config/initializers/acts_as_taggable.rb)4
-rw-r--r--config/initializers/1_settings.rb5
-rw-r--r--config/initializers/fast_gettext.rb1
-rw-r--r--config/routes.rb15
-rw-r--r--config/routes/admin.rb12
-rw-r--r--config/routes/git_http.rb6
-rw-r--r--config/routes/group.rb18
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb16
-rw-r--r--config/routes/repository.rb6
-rw-r--r--config/routes/user.rb28
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--config/webpack.config.js20
-rw-r--r--db/migrate/20170317203554_index_routes_path_for_like.rb5
-rw-r--r--db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb8
-rw-r--r--db/migrate/20170427103502_create_web_hook_logs.rb22
-rw-r--r--db/migrate/20170503185032_index_redirect_routes_path_for_like.rb5
-rw-r--r--db/migrate/20170523091700_add_rss_token_to_users.rb19
-rw-r--r--db/schema.rb22
-rw-r--r--doc/development/i18n_guide.md12
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md3
-rw-r--r--doc/install/kubernetes/gitlab_chart.md36
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/user/group/subgroups/index.md4
-rwxr-xr-xdoc/user/project/integrations/img/webhook_logs.pngbin0 -> 24066 bytes
-rw-r--r--doc/user/project/integrations/jira.md3
-rw-r--r--doc/user/project/integrations/webhooks.md16
-rw-r--r--doc/user/project/milestones/img/progress.pngbin0 -> 23491 bytes
-rw-r--r--doc/user/project/milestones/index.md8
-rw-r--r--features/project/hooks.feature37
-rw-r--r--features/steps/dashboard/dashboard.rb2
-rw-r--r--features/steps/project/hooks.rb75
-rw-r--r--features/steps/project/services.rb6
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/services.rb8
-rw-r--r--lib/api/v3/repositories.rb2
-rw-r--r--lib/constraints/group_url_constrainer.rb6
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/constraints/user_url_constrainer.rb6
-rw-r--r--lib/gitlab/ci/trace/stream.rb51
-rw-r--r--lib/gitlab/database/migration_helpers.rb35
-rw-r--r--lib/gitlab/dependency_linker.rb11
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb75
-rw-r--r--lib/gitlab/dependency_linker/cartfile_linker.rb14
-rw-r--r--lib/gitlab/dependency_linker/cocoapods.rb10
-rw-r--r--lib/gitlab/dependency_linker/composer_json_linker.rb18
-rw-r--r--lib/gitlab/dependency_linker/gemfile_linker.rb23
-rw-r--r--lib/gitlab/dependency_linker/gemspec_linker.rb18
-rw-r--r--lib/gitlab/dependency_linker/godeps_json_linker.rb26
-rw-r--r--lib/gitlab/dependency_linker/json_linker.rb44
-rw-r--r--lib/gitlab/dependency_linker/method_linker.rb39
-rw-r--r--lib/gitlab/dependency_linker/package_json_linker.rb44
-rw-r--r--lib/gitlab/dependency_linker/podfile_linker.rb15
-rw-r--r--lib/gitlab/dependency_linker/podspec_json_linker.rb32
-rw-r--r--lib/gitlab/dependency_linker/podspec_linker.rb24
-rw-r--r--lib/gitlab/dependency_linker/requirements_txt_linker.rb17
-rw-r--r--lib/gitlab/diff/file.rb79
-rw-r--r--lib/gitlab/diff/file_collection/base.rb15
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb3
-rw-r--r--lib/gitlab/diff/highlight.rb6
-rw-r--r--lib/gitlab/etag_caching/router.rb2
-rw-r--r--lib/gitlab/git/diff.rb8
-rw-r--r--lib/gitlab/git/diff_collection.rb41
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb17
-rw-r--r--lib/gitlab/i18n.rb31
-rw-r--r--lib/gitlab/path_regex.rb264
-rw-r--r--lib/gitlab/regex.rb263
-rw-r--r--lib/tasks/tokens.rake10
-rw-r--r--package.json5
-rw-r--r--spec/controllers/application_controller_spec.rb36
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb21
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb21
-rw-r--r--spec/factories/services.rb1
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/factories/web_hook_log.rb14
-rw-r--r--spec/features/admin/admin_hook_logs_spec.rb40
-rw-r--r--spec/features/admin/admin_hooks_spec.rb15
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb15
-rw-r--r--spec/features/atom/dashboard_spec.rb9
-rw-r--r--spec/features/atom/issues_spec.rb23
-rw-r--r--spec/features/atom/users_spec.rb9
-rw-r--r--spec/features/dashboard/activity_spec.rb6
-rw-r--r--spec/features/dashboard/issues_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb2
-rw-r--r--spec/features/dashboard_issues_spec.rb4
-rw-r--r--spec/features/groups/activity_spec.rb8
-rw-r--r--spec/features/groups/issues_spec.rb8
-rw-r--r--spec/features/groups/show_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb8
-rw-r--r--spec/features/issues/form_spec.rb31
-rw-r--r--spec/features/profile_spec.rb15
-rw-r--r--spec/features/projects/activity/rss_spec.rb4
-rw-r--r--spec/features/projects/commit/rss_spec.rb8
-rw-r--r--spec/features/projects/compare_spec.rb6
-rw-r--r--spec/features/projects/issues/rss_spec.rb8
-rw-r--r--spec/features/projects/main/rss_spec.rb4
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb52
-rw-r--r--spec/features/projects/sub_group_issuables_spec.rb32
-rw-r--r--spec/features/projects/tree/rss_spec.rb4
-rw-r--r--spec/features/users/rss_spec.rb4
-rw-r--r--spec/helpers/rss_helper_spec.rb8
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js45
-rw-r--r--spec/javascripts/raven/raven_config_spec.js18
-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/ci/trace/stream_spec.rb43
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb11
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb52
-rw-r--r--spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb74
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb82
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb2
-rw-r--r--spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb66
-rw-r--r--spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb84
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb94
-rw-r--r--spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb53
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb96
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb69
-rw-r--r--spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb87
-rw-r--r--spec/lib/gitlab/dependency_linker_spec.rb72
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb6
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb40
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb83
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/i18n_spec.rb32
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb384
-rw-r--r--spec/lib/gitlab/regex_spec.rb392
-rw-r--r--spec/mailers/notify_spec.rb9
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb4
-rw-r--r--spec/models/hooks/service_hook_spec.rb35
-rw-r--r--spec/models/hooks/system_hook_spec.rb22
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb30
-rw-r--r--spec/models/hooks/web_hook_spec.rb93
-rw-r--r--spec/models/label_spec.rb17
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/models/namespace_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb152
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb35
-rw-r--r--spec/models/user_spec.rb16
-rw-r--r--spec/requests/api/users_spec.rb4
-rw-r--r--spec/routing/admin_routing_spec.rb12
-rw-r--r--spec/routing/project_routing_spec.rb12
-rw-r--r--spec/routing/routing_spec.rb4
-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/web_hook_service_spec.rb137
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb16
-rw-r--r--spec/support/features/rss_shared_examples.rb24
-rw-r--r--spec/support/issuable_shared_examples.rb7
-rw-r--r--spec/support/kubernetes_helpers.rb2
-rw-r--r--spec/support/test_env.rb13
-rw-r--r--spec/tasks/tokens_spec.rb6
-rw-r--r--spec/validators/dynamic_path_validator_spec.rb14
-rw-r--r--spec/workers/process_commit_worker_spec.rb20
-rw-r--r--spec/workers/remove_old_web_hook_logs_worker_spec.rb18
-rw-r--r--yarn.lock218
296 files changed, 4904 insertions, 2033 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 638553d7bf7..dea11bb9f61 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -150,6 +150,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository
build-package:
+ image: ruby:2.3-alpine
before_script: []
services: []
variables:
@@ -486,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..a708d7b224c 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
-
+
# 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/Gemfile.lock b/Gemfile.lock
index 873cd8781ef..dd2c85052f3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -341,7 +341,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
- grpc (1.2.5)
+ grpc (1.3.4)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 2090a7e12d6..c5fffea8bb0 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -123,7 +123,7 @@ import ShortcutsBlob from './shortcuts_blob';
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
- if (gl.FilteredSearchManager) {
+ if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
}
Issuable.init();
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..59c52c1e497 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1212,7 +1212,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/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index da3fb7a6744..ae54fa5f1a9 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -1,4 +1,5 @@
import Raven from 'raven-js';
+import $ from 'jquery';
const IGNORE_ERRORS = [
// Random plugins/extensions
@@ -74,7 +75,7 @@ const RavenConfig = {
},
bindRavenErrors() {
- window.$(document).on('ajaxError.raven', this.handleRavenErrors);
+ $(document).on('ajaxError.raven', this.handleRavenErrors);
},
handleRavenErrors(event, req, config, err) {
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index bacb26734c9..c44892dae3d 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -4,7 +4,7 @@
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
- WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
+ WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
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/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 0db3ac1a60e..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;
@@ -110,6 +110,7 @@
.award-control {
margin: 0 5px 6px 0;
outline: 0;
+ position: relative;
&.disabled {
cursor: default;
@@ -227,8 +228,8 @@
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
- left: 11px;
- bottom: 7px;
+ left: 10px;
+ bottom: 6px;
opacity: 0;
@include transition(opacity, transform);
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f8674b763c8..78f425057eb 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -66,10 +66,10 @@
&.video {
background: $file-image-bg;
text-align: center;
+ padding: 30px;
img,
video {
- padding: 20px;
max-width: 80%;
}
}
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/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 1fd734d279b..ddccfc96819 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -3,12 +3,6 @@
margin: 0;
padding: 0;
- .note-text {
- p:last-child {
- margin-bottom: 0 !important;
- }
- }
-
.system-note {
.note-text {
color: $gl-text-color !important;
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/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 17a4e8fd83e..4db77752c0c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -247,7 +247,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
$file-mode-changed: #777;
-$diff-image-bg: #ddd;
$diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index cfb1df4df84..58715c4c083 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -151,14 +151,10 @@
}
}
}
-
- .text-file.diff-wrap-lines table .line_holder td span {
- white-space: pre-wrap;
- }
}
.image {
- background: $diff-image-bg;
+ background: $file-image-bg;
text-align: center;
padding: 30px;
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/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 51918917329..32d2e9ba4bd 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -14,19 +14,6 @@ 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;
@@ -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,7 +114,7 @@ ul.notes {
.note-awards {
.js-awards-block {
- margin-bottom: 16px;
+ margin-top: 16px;
}
}
@@ -161,7 +149,7 @@ ul.notes {
.system-note {
font-size: 14px;
- padding: 0;
+ padding-left: 0;
clear: both;
@media (min-width: $screen-sm-min) {
@@ -198,11 +186,22 @@ ul.notes {
}
}
- .timeline-content {
- padding: 14px 10px;
+ .timeline-icon {
+ float: left;
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $gray-darkest;
+ position: absolute;
+ left: 0;
+ top: 2px;
+ }
+ }
+
+ .timeline-content {
@media (min-width: $screen-sm-min) {
- margin-left: 20px;
+ margin-left: 30px;
}
}
@@ -385,6 +384,12 @@ ul.notes {
padding-bottom: 0;
}
+.note-header-author-name {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+}
+
.note-headline-light {
display: inline;
@@ -590,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;
+ }
}
}
}
@@ -607,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;
@@ -667,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;
@@ -680,6 +684,10 @@ ul.notes {
.line-resolve-btn {
margin-right: 5px;
+
+ svg {
+ vertical-align: middle;
+ }
}
}
@@ -716,6 +724,10 @@ ul.notes {
}
}
+.line-resolve-text {
+ vertical-align: middle;
+}
+
.discussion-next-btn {
svg {
margin: 0;
@@ -733,9 +745,8 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
// Diff is side by side
- .notes_content.parallel .note-header .note-headline-light {
+ .notes_content.parallel .note-header .note-header-author-name {
display: block;
- position: relative;
}
// Diff is inline
.notes_content .note-header .note-headline-light {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f0bf3d4c267..99745019d5a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -384,10 +384,6 @@ a.deploy-project-label {
}
}
-.last-push-widget {
- margin-top: -1px;
-}
-
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
diff --git a/app/controllers/admin/hook_logs_controller.rb b/app/controllers/admin/hook_logs_controller.rb
new file mode 100644
index 00000000000..aa069b89563
--- /dev/null
+++ b/app/controllers/admin/hook_logs_controller.rb
@@ -0,0 +1,29 @@
+class Admin::HookLogsController < Admin::ApplicationController
+ include HooksExecution
+
+ before_action :hook, only: [:show, :retry]
+ before_action :hook_log, only: [:show, :retry]
+
+ respond_to :html
+
+ def show
+ end
+
+ def retry
+ status, message = hook.execute(hook_log.request_data, hook_log.trigger)
+
+ set_hook_execution_notice(status, message)
+
+ redirect_to edit_admin_hook_path(@hook)
+ end
+
+ private
+
+ def hook
+ @hook ||= SystemHook.find(params[:hook_id])
+ end
+
+ def hook_log
+ @hook_log ||= hook.web_hook_logs.find(params[:id])
+ end
+end
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index ccfe553c89e..b9251e140f8 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,5 +1,7 @@
class Admin::HooksController < Admin::ApplicationController
- before_action :hook, only: :edit
+ include HooksExecution
+
+ before_action :hook_logs, only: :edit
def index
@hooks = SystemHook.all
@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController
end
def test
- data = {
- event_name: "project_create",
- name: "Ruby",
- path: "ruby",
- project_id: 1,
- owner_name: "Someone",
- owner_email: "example@gitlabhq.com"
- }
- hook.execute(data, 'system_hooks')
+ status, message = hook.execute(sample_hook_data, 'system_hooks')
+
+ set_hook_execution_notice(status, message)
redirect_back_or_default
end
@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController
@hook ||= SystemHook.find(params[:id])
end
+ def hook_logs
+ @hook_logs ||=
+ Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ end
+
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController
:url
)
end
+
+ def sample_hook_data
+ {
+ event_name: "project_create",
+ name: "Ruby",
+ path: "ruby",
+ project_id: 1,
+ owner_name: "Someone",
+ owner_email: "example@gitlabhq.com"
+ }
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 8ce9150e4a9..47ce21d238b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
before_action :authenticate_user_from_private_token!
+ before_action :authenticate_user_from_rss_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
@@ -72,13 +73,20 @@ class ApplicationController < ActionController::Base
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
- if user && can?(user, :log_in)
- # Notice we are passing store false, so the user is not
- # actually stored in the session and a token is needed
- # for every request. If you want the token to work as a
- # sign in token, you can simply remove store: false.
- sign_in user, store: false
- end
+ sessionless_sign_in(user)
+ end
+
+ # This filter handles authentication for atom request with an rss_token
+ def authenticate_user_from_rss_token!
+ return unless request.format.atom?
+
+ token = params[:rss_token].presence
+
+ return unless token.present?
+
+ user = User.find_by_rss_token(token)
+
+ sessionless_sign_in(user)
end
def log_exception(exception)
@@ -275,11 +283,17 @@ class ApplicationController < ActionController::Base
request.base_url
end
- def set_locale
- Gitlab::I18n.set_locale(current_user)
+ def set_locale(&block)
+ Gitlab::I18n.with_user_locale(current_user, &block)
+ end
- yield
- ensure
- Gitlab::I18n.reset_locale
+ def sessionless_sign_in(user)
+ if user && can?(user, :log_in)
+ # Notice we are passing store false, so the user is not
+ # actually stored in the session and a token is needed
+ # for every request. If you want the token to work as a
+ # sign in token, you can simply remove store: false.
+ sign_in user, store: false
+ end
end
end
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index 1efa9fe060f..d5388c4cd20 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -8,17 +8,6 @@ module DiffForPath
return render_404 unless diff_file
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
-
- locals = {
- diff_file: diff_file,
- diff_commit: diff_commit,
- diff_refs: diffs.diff_refs,
- blob: blob,
- project: project
- }
-
- render json: { html: view_to_html_string('projects/diffs/_content', locals) }
+ render json: { html: view_to_html_string('projects/diffs/_content', diff_file: diff_file) }
end
end
diff --git a/app/controllers/concerns/hooks_execution.rb b/app/controllers/concerns/hooks_execution.rb
new file mode 100644
index 00000000000..846cd60518f
--- /dev/null
+++ b/app/controllers/concerns/hooks_execution.rb
@@ -0,0 +1,15 @@
+module HooksExecution
+ extend ActiveSupport::Concern
+
+ private
+
+ def set_hook_execution_notice(status, message)
+ if status && status >= 200 && status < 400
+ flash[:notice] = "Hook executed successfully: HTTP #{status}"
+ elsif status
+ flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
+ else
+ flash[:alert] = "Hook execution failed: #{message}"
+ end
+ end
+end
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..3e921a1b1cb 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -165,7 +165,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/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 57e23cea00e..8cd1c47eb3f 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -40,6 +40,14 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_account_path
end
+ def reset_rss_token
+ if current_user.reset_rss_token!
+ flash[:notice] = "RSS token was successfully reset"
+ end
+
+ redirect_to profile_account_path
+ end
+
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 008d2f5815f..88dd600e5fe 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController
if @compare
@commits = @compare.commits
- @start_commit = @compare.start_commit
- @commit = @compare.commit
- @base_commit = @compare.base_commit
-
@diffs = @compare.diffs(diff_options)
- environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
new file mode 100644
index 00000000000..354f0d6db3a
--- /dev/null
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -0,0 +1,33 @@
+class Projects::HookLogsController < Projects::ApplicationController
+ include HooksExecution
+
+ before_action :authorize_admin_project!
+
+ before_action :hook, only: [:show, :retry]
+ before_action :hook_log, only: [:show, :retry]
+
+ respond_to :html
+
+ layout 'project_settings'
+
+ def show
+ end
+
+ def retry
+ status, message = hook.execute(hook_log.request_data, hook_log.trigger)
+
+ set_hook_execution_notice(status, message)
+
+ redirect_to edit_namespace_project_hook_path(@project.namespace, @project, @hook)
+ end
+
+ private
+
+ def hook
+ @hook ||= @project.hooks.find(params[:hook_id])
+ end
+
+ def hook_log
+ @hook_log ||= hook.web_hook_logs.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 86d13a0d222..38bd82841dc 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,7 +1,9 @@
class Projects::HooksController < Projects::ApplicationController
+ include HooksExecution
+
# Authorize
before_action :authorize_admin_project!
- before_action :hook, only: :edit
+ before_action :hook_logs, only: :edit
respond_to :html
@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
- if status && status >= 200 && status < 400
- flash[:notice] = "Hook executed successfully: HTTP #{status}"
- elsif status
- flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
- else
- flash[:alert] = "Hook execution failed: #{message}"
- end
+ set_hook_execution_notice(status, message)
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end
@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController
@hook ||= @project.hooks.find(params[:id])
end
+ def hook_logs
+ @hook_logs ||=
+ Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
+ end
+
def hook_params
params.require(:hook).permit(
:job_events,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 0352065998b..314906b5f09 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -14,7 +14,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
- before_action :define_commit_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :check_if_can_be_merged, only: :show
@@ -130,8 +129,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = true
end
- define_commit_vars
-
render_diff_for_path(@diffs)
end
@@ -500,11 +497,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
- def define_commit_vars
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
- end
-
def define_diff_vars
@merge_request_diff =
if params[:diff_id]
@@ -569,7 +561,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 667f4870c7a..2a0b58fae7c 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController
private
def validate_ref_id
- return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex
+ return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d59d51905a6..5b5cdebe919 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -15,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
- def image_diff_class(diff)
- if diff.deleted_file
- "deleted"
- elsif diff.new_file
- "added"
- else
- nil
- end
- end
-
def commit_to_html(commit, ref, project)
render 'projects/commits/commit',
commit: commit,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4a06ee653ee..4c4fbdd4d39 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -102,14 +102,14 @@ module DiffHelper
].join(' ').html_safe
end
- def commit_for_diff(diff_file)
- return diff_file.content_commit if diff_file.content_commit
+ def diff_file_blob_raw_path(diff_file)
+ namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
+ end
- if diff_file.deleted_file
- @base_commit || @commit.parent || @commit
- else
- @commit
- end
+ def diff_file_old_blob_raw_path(diff_file)
+ sha = diff_file.old_content_sha
+ return unless sha
+ namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
def diff_file_html_data(project, diff_file_path, diff_commit_id)
@@ -120,8 +120,8 @@ module DiffHelper
}
end
- def editable_diff?(diff)
- !diff.deleted_file && @merge_request && @merge_request.source_project
+ def editable_diff?(diff_file)
+ !diff_file.deleted_file? && @merge_request && @merge_request.source_project
end
private
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e5b1e6e8bc7..4e6e6805920 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -69,13 +69,12 @@ module LabelsHelper
end
def render_colored_label(label, label_suffix = '', tooltip: true)
- label_color = label.color || Label::DEFAULT_COLOR
- text_color = text_color_for_bg(label_color)
+ text_color = text_color_for_bg(label.color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
- %(style="background-color: #{label_color}; color: #{text_color}" ) +
+ %(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 98bbcfaaba5..7b7c03142c4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -116,6 +116,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)
diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb
index ea5d2932ef4..9ac4df88dc3 100644
--- a/app/helpers/rss_helper.rb
+++ b/app/helpers/rss_helper.rb
@@ -1,5 +1,5 @@
module RssHelper
def rss_url_options
- { format: :atom, private_token: current_user.try(:private_token) }
+ { format: :atom, rss_token: current_user.try(:rss_token) }
end
end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index d2980db218a..654468bc7fe 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,4 +1,6 @@
class BaseMailer < ActionMailer::Base
+ around_action :render_with_default_locale
+
helper ApplicationHelper
helper MarkupHelper
@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base
private
+ def render_with_default_locale(&block)
+ Gitlab::I18n.with_default_locale(&block)
+ end
+
def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from)
address.display_name = Gitlab.config.gitlab.email_display_name
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 6359f7596b1..f734952fa6c 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -33,14 +33,4 @@ module NoteOnDiff
def created_at_diff?(diff_refs)
false
end
-
- private
-
- def noteable_diff_refs
- if noteable.respond_to?(:diff_sha_refs)
- noteable.diff_sha_refs
- else
- noteable.diff_refs
- end
- end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 1764004078e..2a4cff37566 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -63,7 +63,7 @@ class DiffNote < Note
return false unless supported?
return true if for_commit?
- diff_refs ||= noteable_diff_refs
+ diff_refs ||= noteable.diff_refs
self.position.diff_refs == diff_refs
end
@@ -99,7 +99,7 @@ class DiffNote < Note
self.project,
nil,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: noteable_diff_refs,
+ new_diff_refs: noteable.diff_refs,
paths: self.position.paths
).execute(self)
end
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index eef24052a06..40e43c27f91 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -2,6 +2,6 @@ class ServiceHook < WebHook
belongs_to :service
def execute(data)
- super(data, 'service_hook')
+ WebHookService.new(self, data, 'service_hook').execute
end
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c645805c6da..1584235ab00 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -3,8 +3,4 @@ class SystemHook < WebHook
default_value_for :push_events, false
default_value_for :repository_update_events, true
-
- def async_execute(data, hook_name)
- Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
- end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index a165fdc312f..7503f3739c3 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -1,6 +1,5 @@
class WebHook < ActiveRecord::Base
include Sortable
- include HTTParty
default_value_for :push_events, true
default_value_for :issues_events, false
@@ -13,52 +12,18 @@ class WebHook < ActiveRecord::Base
default_value_for :repository_update_events, false
default_value_for :enable_ssl_verification, true
+ has_many :web_hook_logs, dependent: :destroy
+
scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true) }
- # HTTParty timeout
- default_timeout Gitlab.config.gitlab.webhook_timeout
-
validates :url, presence: true, url: true
def execute(data, hook_name)
- parsed_url = URI.parse(url)
- if parsed_url.userinfo.blank?
- response = WebHook.post(url,
- body: data.to_json,
- headers: build_headers(hook_name),
- verify: enable_ssl_verification)
- else
- post_url = url.gsub("#{parsed_url.userinfo}@", '')
- auth = {
- username: CGI.unescape(parsed_url.user),
- password: CGI.unescape(parsed_url.password)
- }
- response = WebHook.post(post_url,
- body: data.to_json,
- headers: build_headers(hook_name),
- verify: enable_ssl_verification,
- basic_auth: auth)
- end
-
- [response.code, response.to_s]
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
- logger.error("WebHook Error => #{e}")
- [false, e.to_s]
+ WebHookService.new(self, data, hook_name).execute
end
def async_execute(data, hook_name)
- Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name)
- end
-
- private
-
- def build_headers(hook_name)
- headers = {
- 'Content-Type' => 'application/json',
- 'X-Gitlab-Event' => hook_name.singularize.titleize
- }
- headers['X-Gitlab-Token'] = token if token.present?
- headers
+ WebHookService.new(self, data, hook_name).async_execute
end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
new file mode 100644
index 00000000000..2738b229d84
--- /dev/null
+++ b/app/models/hooks/web_hook_log.rb
@@ -0,0 +1,13 @@
+class WebHookLog < ActiveRecord::Base
+ belongs_to :web_hook
+
+ serialize :request_headers, Hash
+ serialize :request_data, Hash
+ serialize :response_headers, Hash
+
+ validates :web_hook, presence: true
+
+ def success?
+ response_status =~ /^2/
+ end
+end
diff --git a/app/models/label.rb b/app/models/label.rb
index ddddb6bdf8f..074239702f8 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -133,6 +133,10 @@ class Label < ActiveRecord::Base
template
end
+ def color
+ super || DEFAULT_COLOR
+ end
+
def text_color
LabelsHelper.text_color_for_bg(self.color)
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index d7c627432d2..ebf8fb92ab5 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -61,7 +61,7 @@ class LegacyDiffNote < Note
return true if for_commit?
return true unless diff_line
return false unless noteable
- return false if diff_refs && diff_refs != noteable_diff_refs
+ return false if diff_refs && diff_refs != noteable.diff_refs
noteable_diff = find_noteable_diff
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2eec013fa9d..356af776b8d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -245,19 +245,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
- # but we need to get a commit for the "View file @ ..." link by deleted files,
- # so we find the likely one if we can't get the actual one.
- # This will not be the actual base commit if the target branch was merged into
- # the source branch after the merge request was created, but it is good enough
- # for the specific purpose of linking to a commit.
- # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
- # true base commit, so we can't simply have `#diff_base_commit` fall back on
- # this method.
- def likely_diff_base_commit
- first_commit.try(:parent) || first_commit
- end
-
def diff_start_commit
if persisted?
merge_request_diff.start_commit
@@ -322,21 +309,14 @@ class MergeRequest < ActiveRecord::Base
end
def diff_refs
- return unless diff_start_commit || diff_base_commit
-
- Gitlab::Diff::DiffRefs.new(
- base_sha: diff_base_sha,
- start_sha: diff_start_sha,
- head_sha: diff_head_sha
- )
- end
-
- # Return diff_refs instance trying to not touch the git repository
- def diff_sha_refs
- if merge_request_diff && merge_request_diff.diff_refs_by_sha?
+ if persisted?
merge_request_diff.diff_refs
else
- diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: diff_base_sha,
+ start_sha: diff_start_sha,
+ head_sha: diff_head_sha
+ )
end
end
@@ -870,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
end
def has_complete_diff_refs?
- diff_sha_refs && diff_sha_refs.complete?
+ diff_refs && diff_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6e3917a10a3..1bd61c1d465 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -150,6 +150,29 @@ class MergeRequestDiff < ActiveRecord::Base
)
end
+ # MRs created before 8.4 don't store their true diff refs (start and base),
+ # but we need to get a commit SHA for the "View file @ ..." link by a file,
+ # so we use an approximation of the diff refs if we can't get the actual one.
+ #
+ # These will not be the actual diff refs if the target branch was merged into
+ # the source branch after the merge request was created, but it is good enough
+ # for the specific purpose of linking to a commit.
+ #
+ # It is not good enough for highlighting diffs, so we can't simply pass
+ # these as `diff_refs.`
+ def fallback_diff_refs
+ real_refs = diff_refs
+ return real_refs if real_refs
+
+ likely_base_commit_sha = (first_commit&.parent || first_commit)&.sha
+
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: likely_base_commit_sha,
+ start_sha: safe_start_commit_sha,
+ head_sha: head_commit_sha
+ )
+ end
+
def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index fbf2a0a75ca..a0314bf9e49 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base
presence: true,
dynamic_path: true,
length: { maximum: 255 },
- format: { with: Gitlab::Regex.project_path_format_regex,
- message: Gitlab::Regex.project_path_regex_message },
+ format: { with: Gitlab::PathRegex.project_path_format_regex,
+ message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base
end
def reference_pattern
- name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
-
%r{
- ((?<namespace>#{name_pattern})\/)?
- (?<project>#{name_pattern})
+ ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
+ (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
end
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/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index b2494a0be6e..8977a7cdafe 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -77,6 +77,14 @@ class KubernetesService < DeploymentService
]
end
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kubeclient!
@@ -91,7 +99,7 @@ class KubernetesService < DeploymentService
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
+ { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }
]
if ca_pem.present?
@@ -110,7 +118,7 @@ class KubernetesService < DeploymentService
with_reactive_cache do |data|
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
- flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
+ flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.
each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -124,7 +132,7 @@ class KubernetesService < DeploymentService
# Store as hashes, rather than as third-party types
pods = begin
- kubeclient.get_pods(namespace: namespace).as_json
+ kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
@@ -142,20 +150,12 @@ class KubernetesService < DeploymentService
default_namespace || TEMPLATE_PLACEHOLDER
end
- def namespace_variable
- if namespace.present?
- namespace
- else
- default_namespace
- end
- end
-
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
- raise "Incomplete settings" unless api_url && namespace && token
+ raise "Incomplete settings" unless api_url && actual_namespace && token
::Kubeclient::Client.new(
join_api_url(api_path),
diff --git a/app/models/user.rb b/app/models/user.rb
index 837ab78228b..625ba90002b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,6 +15,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
+ add_authentication_token_field :rss_token
default_value_for :admin, false
default_value_for(:external) { current_application_settings.user_default_external }
@@ -367,7 +368,7 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
- (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
+ (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x
end
@@ -918,13 +919,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
@@ -1004,6 +1005,13 @@ class User < ActiveRecord::Base
save
end
+ # each existing user needs to have an `rss_token`.
+ # we do this on read since migrating all existing users is not a feasible
+ # solution.
+ def rss_token
+ ensure_rss_token!
+ end
+
protected
# override, from Devise::Validatable
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/web_hook_service.rb b/app/services/web_hook_service.rb
new file mode 100644
index 00000000000..4241b912d5b
--- /dev/null
+++ b/app/services/web_hook_service.rb
@@ -0,0 +1,120 @@
+class WebHookService
+ class InternalErrorResponse
+ attr_reader :body, :headers, :code
+
+ def initialize
+ @headers = HTTParty::Response::Headers.new({})
+ @body = ''
+ @code = 'internal error'
+ end
+ end
+
+ include HTTParty
+
+ # HTTParty timeout
+ default_timeout Gitlab.config.gitlab.webhook_timeout
+
+ attr_accessor :hook, :data, :hook_name
+
+ def initialize(hook, data, hook_name)
+ @hook = hook
+ @data = data
+ @hook_name = hook_name
+ end
+
+ def execute
+ start_time = Time.now
+
+ response = if parsed_url.userinfo.blank?
+ make_request(hook.url)
+ else
+ make_request_with_auth
+ end
+
+ log_execution(
+ trigger: hook_name,
+ url: hook.url,
+ request_data: data,
+ response: response,
+ execution_duration: Time.now - start_time
+ )
+
+ [response.code, response.to_s]
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+ log_execution(
+ trigger: hook_name,
+ url: hook.url,
+ request_data: data,
+ response: InternalErrorResponse.new,
+ execution_duration: Time.now - start_time,
+ error_message: e.to_s
+ )
+
+ Rails.logger.error("WebHook Error => #{e}")
+
+ [nil, e.to_s]
+ end
+
+ def async_execute
+ Sidekiq::Client.enqueue(WebHookWorker, hook.id, data, hook_name)
+ end
+
+ private
+
+ def parsed_url
+ @parsed_url ||= URI.parse(hook.url)
+ end
+
+ def make_request(url, basic_auth = false)
+ self.class.post(url,
+ body: data.to_json,
+ headers: build_headers(hook_name),
+ verify: hook.enable_ssl_verification,
+ basic_auth: basic_auth)
+ end
+
+ def make_request_with_auth
+ post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
+ basic_auth = {
+ username: CGI.unescape(parsed_url.user),
+ password: CGI.unescape(parsed_url.password)
+ }
+ make_request(post_url, basic_auth)
+ end
+
+ def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
+ # logging for ServiceHook's is not available
+ return if hook.is_a?(ServiceHook)
+
+ WebHookLog.create(
+ web_hook: hook,
+ trigger: trigger,
+ url: url,
+ execution_duration: execution_duration,
+ request_headers: build_headers(hook_name),
+ request_data: request_data,
+ response_headers: format_response_headers(response),
+ response_body: response.body,
+ response_status: response.code,
+ internal_error_message: error_message
+ )
+ end
+
+ def build_headers(hook_name)
+ @headers ||= begin
+ {
+ 'Content-Type' => 'application/json',
+ 'X-Gitlab-Event' => hook_name.singularize.titleize
+ }.tap do |hash|
+ hash['X-Gitlab-Token'] = hook.token if hook.token.present?
+ end
+ end
+ end
+
+ # Make response headers more stylish
+ # Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] }
+ # This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' }
+ def format_response_headers(response)
+ response.headers.each_capitalized.to_h
+ end
+end
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
index 8d4d7180baf..6819886ebf4 100644
--- a/app/validators/dynamic_path_validator.rb
+++ b/app/validators/dynamic_path_validator.rb
@@ -3,16 +3,20 @@
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
-# Values are checked for formatting and exclusion from a list of reserved path
+# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
class << self
- def valid_namespace_path?(path)
- "#{path}/" =~ Gitlab::Regex.full_namespace_path_regex
+ def valid_user_path?(path)
+ "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
+ end
+
+ def valid_group_path?(path)
+ "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end
def valid_project_path?(path)
- "#{path}/" =~ Gitlab::Regex.full_project_path_regex
+ "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end
end
@@ -24,14 +28,16 @@ class DynamicPathValidator < ActiveModel::EachValidator
case record
when Project
self.class.valid_project_path?(full_path)
- else
- self.class.valid_namespace_path?(full_path)
+ when Group
+ self.class.valid_group_path?(full_path)
+ else # User or non-Group Namespace
+ self.class.valid_user_path?(full_path)
end
end
def validate_each(record, attribute, value)
- unless value =~ Gitlab::Regex.namespace_regex
- record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
+ unless value =~ Gitlab::PathRegex.namespace_format_regex
+ record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
return
end
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
new file mode 100644
index 00000000000..7dd9943190f
--- /dev/null
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -0,0 +1,37 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Recent Deliveries
+ %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
+ .col-lg-9
+ - if hook_logs.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Trigger
+ %th URL
+ %th Elapsed time
+ %th Request time
+ %th
+ - hook_logs.each do |hook_log|
+ %tr
+ %td
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+ %td
+ = truncate(hook_log.url, length: 50)
+ %td.light
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ %td.light
+ = time_ago_with_tooltip(hook_log.created_at)
+ %td
+ = link_to 'View details', admin_hook_hook_log_path(hook, hook_log)
+
+ = paginate hook_logs, theme: 'gitlab'
+
+ - else
+ .settings-message.text-center
+ You don't have any webhooks deliveries
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
new file mode 100644
index 00000000000..56127bacda2
--- /dev/null
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -0,0 +1,10 @@
+- page_title 'Request details'
+%h3.page-title
+ Request details
+
+%hr
+
+= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+
+= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
+
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 0777f5e2629..0e35a1905bf 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -12,3 +12,9 @@
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
+ = link_to 'Test hook', test_admin_hook_path(@hook), class: 'btn btn-default'
+ = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+
+%hr
+
+= render partial: 'admin/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs }
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index ae918086a57..c7b63d9de98 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -20,7 +20,7 @@
%ul.content-list
- profiles.each do |profile|
%li
- = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true}
+ = link_to profile.time.to_s(:long), admin_requests_profile_path(profile)
- else
%p
No profiles found
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/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index a2f6a7ab1cb..d696577278d 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index c3f55ff821f..70042dee20f 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion), show_toggle: false
+ = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
.diff-content.code.js-syntax-highlight
%table
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.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 d068c895fa3..f6132464910 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -17,7 +17,7 @@
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
Messages
- = nav_link(controller: :hooks) do
+ = nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
System Hooks
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 02eb7c8462c..546376aeed8 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -27,40 +27,38 @@
%h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @message.diffs.each do |diff|
+ - @message.diffs.each do |diff_file|
%li.file-stats
- %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- - if diff.deleted_file
+ %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff_file.file_path)}" }
+ - if diff_file.deleted_file?
%span.deleted-file
&minus;
- = diff.old_path
- - elsif diff.renamed_file
- = diff.old_path
+ = diff_file.old_path
+ - elsif diff_file.renamed_file?
+ = diff_file.old_path
&rarr;
- = diff.new_path
- - elsif diff.new_file
+ = diff_file.new_path
+ - elsif diff_file.new_file?
%span.new-file
&#43;
- = diff.new_path
+ = diff_file.new_path
- else
- = diff.new_path
+ = diff_file.new_path
- unless @message.disable_diffs?
- - diff_files = @message.diffs
-
- if @message.compare_timeout
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- - diff_files.each do |diff_file|
+ - @message.diffs.each do |diff_file|
- file_hash = hexdigest(diff_file.file_path)
%li{ id: file_hash }
%a{ href: @message.target_url + "##{file_hash}" }<
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
%strong<
= diff_file.old_path
deleted
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
%strong<
= diff_file.old_path
&rarr;
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 5ac23aa3997..895d8807e47 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -15,15 +15,15 @@
\
#{pluralize @message.diffs_count, "changed file"}:
\
- - @message.diffs.each do |diff|
- - if diff.deleted_file
- \- − #{diff.old_path}
- - elsif diff.renamed_file
- \- #{diff.old_path} → #{diff.new_path}
- - elsif diff.new_file
- \- + #{diff.new_path}
+ - @message.diffs.each do |diff_file|
+ - if diff_file.deleted_file?
+ \- − #{diff_file.old_path}
+ - elsif diff_file.renamed_file?
+ \- #{diff_file.old_path} → #{diff_file.new_path}
+ - elsif diff_file.new_file?
+ \- + #{diff_file.new_path}
- else
- \- #{diff.new_path}
+ \- #{diff_file.new_path}
- unless @message.disable_diffs?
- if @message.compare_timeout
\
@@ -36,9 +36,9 @@
- @message.diffs.each do |diff_file|
\
\=====================================
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
#{diff_file.old_path} deleted
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
#{diff_file.old_path} → #{diff_file.new_path}
- else
= diff_file.new_path
diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml
new file mode 100644
index 00000000000..c31a4a8ecd4
--- /dev/null
+++ b/app/views/profiles/accounts/_reset_token.html.haml
@@ -0,0 +1,11 @@
+- name = label.parameterize
+- attribute = name.underscore
+
+.reset-action
+ %p.cgray
+ = label_tag name, label, class: "label-light"
+ = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ = help_text
+ .prepend-top-default
+ = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 73f33e69d68..a319b18e507 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -8,35 +8,17 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- = incoming_email_token_enabled? ? "Private Tokens" : "Private Token"
+ Private Tokens
%p
- Keep
- = incoming_email_token_enabled? ? "these tokens" : "this token"
- secret, anyone with access to them can interact with GitLab as if they were you.
+ Keep these tokens secret, anyone with access to them can interact with
+ GitLab as if they were you.
.col-lg-9.private-tokens-reset
- .reset-action
- %p.cgray
- - if current_user.private_token
- = label_tag "private-token", "Private token", class: "label-light"
- = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()"
- - else
- %span You don't have one yet. Click generate to fix it.
- %p.help-block
- Your private token is used to access the API and Atom feeds without username/password authentication.
- .prepend-top-default
- - if current_user.private_token
- = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token"
- - else
- = f.submit 'Generate', class: "btn btn-default"
+ = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
+
+ = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
+
- if incoming_email_token_enabled?
- .reset-action
- %p.cgray
- = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light'
- = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()"
- %p.help-block
- Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.
- .prepend-top-default
- = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token"
+ = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
%hr
.row.prepend-top-default
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/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index c781e423c4d..c7e22a0b4ec 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -1,12 +1,12 @@
-.diff-content.diff-wrap-lines
- -# Skip all non non-supported blobs
- - return unless blob.respond_to?(:text?)
+- blob = diff_file.blob
+
+.diff-content
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob.too_large?
.nothing-here-block The file could not be displayed because it is too large.
- elsif blob.readable_text?
- - if !project.repository.diffable?(blob)
+ - if !diff_file.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
@@ -15,20 +15,13 @@
%a.click-to-expand
Click to expand it.
- elsif diff_file.diff_lines.length > 0
- - total_lines = 0
- - if blob.lines.any?
- - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size
- - if diff_view == :parallel
- = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
+ = render "projects/diffs/viewers/text", diff_file: diff_file
- else
- if diff_file.mode_changed?
.nothing-here-block File mode changed
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
+ = render "projects/diffs/viewers/image", diff_file: diff_file
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 71a1b9e6c05..4768438c29e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -23,12 +23,4 @@
= render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- - diff_files.each_with_index do |diff_file|
- - diff_commit = commit_for_diff(diff_file)
- - blob = diff_file.blob(diff_commit)
- - next unless blob
- - blob.load_all_data!(diffs.project.repository) unless blob.too_large?
- - file_hash = hexdigest(diff_file.file_path)
-
- = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
+ = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index f22b385fc0f..b5aea217384 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,10 +1,12 @@
- environment = local_assigns.fetch(:environment, nil)
-.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
+- file_hash = hexdigest(diff_file.file_path)
+.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) }
.js-file-title.file-title-flex-parent
.file-header-content
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
+ = render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
- unless diff_file.submodule?
+ - blob = diff_file.blob
.file-actions.hidden-xs
- if blob.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
@@ -15,9 +17,9 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_button(diff_commit.id, diff_file.new_path, project)
- = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
+ = view_file_button(diff_file.content_sha, diff_file.file_path, project)
+ = view_on_environment_button(diff_file.content_sha, diff_file.file_path, environment) if environment
= render 'projects/fork_suggestion'
- = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
+ = render 'projects/diffs/content', diff_file: diff_file
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 4e4fdb73ae3..73c316472e3 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -3,19 +3,20 @@
- if show_toggle
%i.fa.diff-toggle-caret.fa-fw
-- if defined?(blob) && blob && diff_file.submodule?
+- if diff_file.submodule?
+ - blob = diff_file.blob
%span
= icon('archive fw')
%strong.file-title-name
- = submodule_link(blob, diff_commit.id, project.repository)
+ = submodule_link(blob, diff_file.content_sha, diff_file.repository)
= copy_file_path_button(blob.path)
- else
= conditional_link_to url.present?, url do
= blob_icon diff_file.b_mode, diff_file.file_path
- - if diff_file.renamed_file
+ - if diff_file.renamed_file?
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path
@@ -23,12 +24,13 @@
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path
- else
- %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
- = diff_file.new_path
- - if diff_file.deleted_file
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.file_path, container: 'body' } }
+ = diff_file.file_path
+
+ - if diff_file.deleted_file?
deleted
- = copy_file_path_button(diff_file.new_path)
+ = copy_file_path_button(diff_file.file_path)
- if diff_file.mode_changed?
%small
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
deleted file mode 100644
index ca10921c5e2..00000000000
--- a/app/views/projects/diffs/_image.html.haml
+++ /dev/null
@@ -1,69 +0,0 @@
-- diff = diff_file.diff
-- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))
-// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
-- if diff_file.old_ref
- - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))
-
-- if diff.renamed_file || diff.new_file || diff.deleted_file
- .image
- %span.wrap
- .frame{ class: image_diff_class(diff) }
- %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
- %p.image-info= number_to_human_size(file.size)
-- else
- .image
- .two-up.view
- %span.wrap
- .frame.deleted
- %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
- %img{ src: old_file_raw_path, alt: diff.old_path }
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(old_file.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
- %span.wrap
- .frame.added
- %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
- %img{ src: file_raw_path, alt: diff.new_path }
- %p.image-info.hide
- %span.meta-filesize= number_to_human_size(file.size)
- |
- %b W:
- %span.meta-width
- |
- %b H:
- %span.meta-height
-
- .swipe.view.hide
- .swipe-frame
- .frame.deleted
- %img{ src: old_file_raw_path, alt: diff.old_path }
- .swipe-wrap
- .frame.added
- %img{ src: file_raw_path, alt: diff.new_path }
- %span.swipe-bar
- %span.top-handle
- %span.bottom-handle
-
- .onion-skin.view.hide
- .onion-skin-frame
- .frame.deleted
- %img{ src: old_file_raw_path, alt: diff.old_path }
- .frame.added
- %img{ src: file_raw_path, alt: diff.new_path }
- .controls
- .transparent
- .drag-track
- .dragger{ :style => "left: 0px;" }
- .opaque
-
-
- .view-modes.hide
- %ul.view-modes-menu
- %li.two-up{ data: { mode: 'two-up' } } 2-up
- %li.swipe{ data: { mode: 'swipe' } } Swipe
- %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 45c95f7ab6a..8e5f4d2573d 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -49,7 +49,7 @@
- if discussions_left || discussions_right
= render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right
- - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
+ - if !diff_file.new_file? && !diff_file.deleted_file? && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines
%tr.line_holder.parallel
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index fd4f3c8d3cc..e69c7f20d49 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -12,19 +12,19 @@
- diff_files.each do |diff_file|
- file_hash = hexdigest(diff_file.file_path)
%li
- - if diff_file.deleted_file
+ - if diff_file.deleted_file?
%span.deleted-file
%a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
- - elsif diff_file.renamed_file
+ - elsif diff_file.renamed_file?
%span.renamed-file
%a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- - elsif diff_file.new_file
+ - elsif diff_file.new_file?
%span.new-file
%a{ href: "##{file_hash}" }
%i.fa.fa-plus
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 5f3968b6709..e8a5e63e59e 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,13 +3,13 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
+%table.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
locals: { diff_file: diff_file, discussions: @grouped_diff_discussions }
- - if !diff_file.new_file && !diff_file.deleted_file && diff_file.highlighted_diff_lines.any?
+ - if !diff_file.new_file? && !diff_file.deleted_file? && diff_file.highlighted_diff_lines.any?
- last_line = diff_file.highlighted_diff_lines.last
- if last_line.new_pos < total_lines
%tr.line_holder
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
new file mode 100644
index 00000000000..ea75373581e
--- /dev/null
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -0,0 +1,68 @@
+- blob = diff_file.blob
+- old_blob = diff_file.old_blob
+- blob_raw_path = diff_file_blob_raw_path(diff_file)
+- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file)
+
+- if diff_file.new_file? || diff_file.deleted_file?
+ .image
+ %span.wrap
+ .frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
+ %img{ src: blob_raw_path, alt: diff_file.file_path }
+ %p.image-info= number_to_human_size(blob.size)
+- else
+ .image
+ .two-up.view
+ %span.wrap
+ .frame.deleted
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(old_blob.size)
+ |
+ %b W:
+ %span.meta-width
+ |
+ %b H:
+ %span.meta-height
+ %span.wrap
+ .frame.added
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.new_path)) }
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %p.image-info.hide
+ %span.meta-filesize= number_to_human_size(blob.size)
+ |
+ %b W:
+ %span.meta-width
+ |
+ %b H:
+ %span.meta-height
+
+ .swipe.view.hide
+ .swipe-frame
+ .frame.deleted
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ .swipe-wrap
+ .frame.added
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %span.swipe-bar
+ %span.top-handle
+ %span.bottom-handle
+
+ .onion-skin.view.hide
+ .onion-skin-frame
+ .frame.deleted
+ %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ .frame.added
+ %img{ src: blob_raw_path, alt: diff_file.new_path }
+ .controls
+ .transparent
+ .drag-track
+ .dragger{ :style => "left: 0px;" }
+ .opaque
+
+
+ .view-modes.hide
+ %ul.view-modes-menu
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/viewers/_text.html.haml b/app/views/projects/diffs/viewers/_text.html.haml
new file mode 100644
index 00000000000..e4b89671724
--- /dev/null
+++ b/app/views/projects/diffs/viewers/_text.html.haml
@@ -0,0 +1,8 @@
+- blob = diff_file.blob
+- blob.load_all_data!(diff_file.repository)
+- total_lines = blob.lines.size
+- total_lines -= 1 if total_lines > 0 && blob.lines.last.blank?
+- if diff_view == :parallel
+ = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines
+- else
+ = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
new file mode 100644
index 00000000000..6962b223451
--- /dev/null
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -0,0 +1,37 @@
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Recent Deliveries
+ %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
+ .col-lg-9
+ - if hook_logs.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Trigger
+ %th URL
+ %th Elapsed time
+ %th Request time
+ %th
+ - hook_logs.each do |hook_log|
+ %tr
+ %td
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+ %td
+ = truncate(hook_log.url, length: 50)
+ %td.light
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ %td.light
+ = time_ago_with_tooltip(hook_log.created_at)
+ %td
+ = link_to 'View details', namespace_project_hook_hook_log_path(project.namespace, project, hook, hook_log)
+
+ = paginate hook_logs, theme: 'gitlab'
+
+ - else
+ .settings-message.text-center
+ You don't have any webhooks deliveries
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
new file mode 100644
index 00000000000..2eabe92f8eb
--- /dev/null
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -0,0 +1,11 @@
+= render 'projects/settings/head'
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Request details
+ .col-lg-9
+
+ = link_to 'Resend Request', retry_namespace_project_hook_hook_log_path(@project.namespace, @project, @hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+
+ = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index 7998713be1f..fd382c1d63f 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title 'Integrations'
= render 'projects/settings/head'
.row.prepend-top-default
@@ -10,5 +11,12 @@
.col-lg-9.append-bottom-default
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+
= f.submit 'Save changes', class: 'btn btn-create'
+ = link_to 'Test hook', test_namespace_project_hook_path(@project.namespace, @project, @hook), class: 'btn btn-default'
+ = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+
+%hr
+
+= render partial: 'projects/hook_logs/index', locals: { hook: @hook, hook_logs: @hook_logs, project: @project }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c9ecfc81266..8b095f4ca10 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'shared/notes/notes_with_form', :autocomplete => true
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 0e928bfbe6d..67403c36d7f 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -33,9 +33,9 @@
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
%li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
@@ -46,8 +46,8 @@
- if can_update_issue
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
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/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
index faed65d6588..00bd563999f 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -14,7 +14,7 @@
%span
Members
- if can_edit
- = nav_link(controller: [:integrations, :services, :hooks]) do
+ = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
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/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 90ae3f06a98..8d5b5129454 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -15,7 +15,7 @@
%strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
- pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
+ pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
new file mode 100644
index 00000000000..af6a499fadb
--- /dev/null
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -0,0 +1,44 @@
+%p
+ %strong Request URL:
+ POST
+ = hook_log.url
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+
+%p
+ %strong Trigger:
+ %td.hidden-xs
+ %span.label.label-gray.deploy-project-label
+ = hook_log.trigger.singularize.titleize
+%p
+ %strong Elapsed time:
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+%p
+ %strong Request time:
+ = time_ago_with_tooltip(hook_log.created_at)
+
+%hr
+
+- if hook_log.internal_error_message.present?
+ .bs-callout.bs-callout-danger
+ = hook_log.internal_error_message
+
+%h5 Request headers:
+%pre
+ - hook_log.request_headers.each do |k,v|
+ <strong>#{k}:</strong> #{v}
+ %br
+
+%h5 Request body:
+%pre
+ :plain
+ #{JSON.pretty_generate(hook_log.request_data)}
+%h5 Response headers:
+%pre
+ - hook_log.response_headers.each do |k,v|
+ <strong>#{k}:</strong> #{v}
+ %br
+
+%h5 Response body:
+%pre
+ :plain
+ #{hook_log.response_body}
diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml
new file mode 100644
index 00000000000..b4ea8e6f952
--- /dev/null
+++ b/app/views/shared/hook_logs/_status_label.html.haml
@@ -0,0 +1,3 @@
+- label_status = hook_log.success? ? 'label-success' : 'label-danger'
+%span{ class: "label #{label_status}" }
+ = hook_log.response_status
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/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index a7bf610b9c7..1e34b7c1e76 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -18,7 +18,7 @@
.note-header
.note-header-info
%a{ href: user_path(note.author) }
- %span.hidden-xs
+ %span.note-header-author-name
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
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/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 2b70d70e360..c587155bc4f 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -71,7 +71,7 @@
= @user.location
- unless @user.organization.blank?
.profile-link-holder.middle-dot-divider
- = icon('building')
+ = icon('briefcase')
= @user.organization
- if @user.bio.present?
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index d6ed0e253ad..fe6a49976e0 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -17,6 +17,7 @@ class ProcessCommitWorker
project = Project.find_by(id: project_id)
return unless project
+ return if commit_exists_in_upstream?(project, commit_hash)
user = User.find_by(id: user_id)
@@ -24,8 +25,6 @@ class ProcessCommitWorker
commit = build_commit(project, commit_hash)
- return unless commit.matches_cross_reference_regex?
-
author = commit.author || user
process_commit_message(project, commit, user, author, default)
@@ -76,4 +75,16 @@ class ProcessCommitWorker
Commit.from_hash(hash, project)
end
+
+ private
+
+ # Avoid reprocessing commits that already exist in the upstream
+ # when project is forked. This will also prevent duplicated system notes.
+ def commit_exists_in_upstream?(project, commit_hash)
+ return false unless project.forked?
+
+ upstream_project = project.forked_from_project
+ commit_id = commit_hash.with_indifferent_access[:id]
+ upstream_project.commit(commit_id).present?
+ end
end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
new file mode 100644
index 00000000000..555e1bb8691
--- /dev/null
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -0,0 +1,10 @@
+class RemoveOldWebHookLogsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ WEB_HOOK_LOG_LIFETIME = 2.days
+
+ def perform
+ WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
+ end
+end
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
deleted file mode 100644
index 55d4e7d6dab..00000000000
--- a/app/workers/system_hook_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class SystemHookWorker
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
-
- sidekiq_options retry: 4
-
- def perform(hook_id, data, hook_name)
- SystemHook.find(hook_id).execute(data, hook_name)
- end
-end
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/web_hook_worker.rb
index d973e662ff2..ad5ddf02a12 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,11 +1,13 @@
-class ProjectWebHookWorker
+class WebHookWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
sidekiq_options retry: 4
def perform(hook_id, data, hook_name)
+ hook = WebHook.find(hook_id)
data = data.with_indifferent_access
- WebHook.find(hook_id).execute(data, hook_name)
+
+ WebHookService.new(hook, data, hook_name).execute
end
end
diff --git a/changelogs/unreleased/12614-fix-long-message-from-mr.yml b/changelogs/unreleased/12614-fix-long-message-from-mr.yml
new file mode 100644
index 00000000000..30408ea4216
--- /dev/null
+++ b/changelogs/unreleased/12614-fix-long-message-from-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Implement web hook logging
+merge_request: 11027
+author: Alexander Randa
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/32799-remove-no_turbolink-attribute-from-haml.yml b/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml
new file mode 100644
index 00000000000..9c1c1fe77f2
--- /dev/null
+++ b/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml
@@ -0,0 +1,4 @@
+---
+title: Remove redundant data-turbolink attributes from links
+merge_request: 11672
+author: blackst0ne
diff --git a/changelogs/unreleased/32807-company-icon.yml b/changelogs/unreleased/32807-company-icon.yml
new file mode 100644
index 00000000000..718108d3733
--- /dev/null
+++ b/changelogs/unreleased/32807-company-icon.yml
@@ -0,0 +1,4 @@
+---
+title: Use briefcase icon for company in profile page
+merge_request:
+author:
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/dm-more-dependency-linkers.yml b/changelogs/unreleased/dm-more-dependency-linkers.yml
new file mode 100644
index 00000000000..12d45e71e85
--- /dev/null
+++ b/changelogs/unreleased/dm-more-dependency-linkers.yml
@@ -0,0 +1,4 @@
+---
+title: Autolink package names in more dependency files
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-rss-scoped-token.yml b/changelogs/unreleased/feature-rss-scoped-token.yml
new file mode 100644
index 00000000000..740d8778be2
--- /dev/null
+++ b/changelogs/unreleased/feature-rss-scoped-token.yml
@@ -0,0 +1,4 @@
+---
+title: Expose atom links with an RSS token instead of using the private token
+merge_request: 11647
+author: Alexis Reigel
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/fix-terminals-support-for-kubernetes-service.yml b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml
new file mode 100644
index 00000000000..fb91da9510c
--- /dev/null
+++ b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml
@@ -0,0 +1,4 @@
+---
+title: Fix terminals support for Kubernetes Service
+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/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
new file mode 100644
index 00000000000..7bcbc647fcb
--- /dev/null
+++ b/changelogs/unreleased/issue_19262.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent commits from upstream repositories to be re-processed by forks
+merge_request:
+author:
diff --git a/config/application.rb b/config/application.rb
index 95ba6774916..b0533759252 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -65,6 +65,7 @@ module Gitlab
hook
import_url
incoming_email_token
+ rss_token
key
otp_attempt
password
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/1_settings.rb b/config/initializers/1_settings.rb
index 5a90830b5b3..4fb4baf631f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -368,11 +368,14 @@ Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_random_weekly_time)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
-# Every day at 00:30
Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['schedule_update_user_activity_worker']['cron'] ||= '30 0 * * *'
Settings.cron_jobs['schedule_update_user_activity_worker']['job_class'] = 'ScheduleUpdateUserActivityWorker'
+Settings.cron_jobs['remove_old_web_hook_logs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_old_web_hook_logs_worker']['cron'] ||= '40 0 * * *'
+Settings.cron_jobs['remove_old_web_hook_logs_worker']['job_class'] = 'RemoveOldWebHookLogsWorker'
+
#
# GitLab Shell
#
diff --git a/config/initializers/fast_gettext.rb b/config/initializers/fast_gettext.rb
index a69fe0c902e..eb589ecdb52 100644
--- a/config/initializers/fast_gettext.rb
+++ b/config/initializers/fast_gettext.rb
@@ -1,5 +1,6 @@
FastGettext.add_text_domain 'gitlab', path: File.join(Rails.root, 'locale'), type: :po
FastGettext.default_text_domain = 'gitlab'
FastGettext.default_available_locales = Gitlab::I18n.available_locales
+FastGettext.default_locale = :en
I18n.available_locales = Gitlab::I18n.available_locales
diff --git a/config/routes.rb b/config/routes.rb
index 2584981bb04..846054e6917 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,5 @@
require 'sidekiq/web'
require 'sidekiq/cron/web'
-require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
concern :access_requestable do
@@ -85,20 +84,6 @@ Rails.application.routes.draw do
root to: "root#index"
- # Since group show page is wildcard routing
- # we want all other routing to be checked before matching this one
- constraints(GroupUrlConstrainer.new) do
- scope(path: '*id',
- as: :group,
- constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ },
- controller: :groups) do
- get '/', action: :show
- patch '/', action: :update
- put '/', action: :update
- delete '/', action: :destroy
- end
- end
-
draw :test if Rails.env.test?
get '*unmatched_route', to: 'application#route_not_found'
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index b1b6ef33a47..c20581b1333 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -36,7 +36,7 @@ namespace :admin do
scope(path: 'groups/*id',
controller: :groups,
- constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
+ constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
scope(as: :group) do
put :members_update
@@ -54,6 +54,12 @@ namespace :admin do
member do
get :test
end
+
+ resources :hook_logs, only: [:show] do
+ member do
+ get :retry
+ end
+ end
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
@@ -70,10 +76,10 @@ namespace :admin do
scope(path: 'projects/*namespace_id',
as: :namespace,
- constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do
+ constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
resources(:projects,
path: '/',
- constraints: { id: Gitlab::Regex.project_route_regex },
+ constraints: { id: Gitlab::PathRegex.project_route_regex },
only: [:show]) do
member do
diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb
index cdf658c3e4a..a53c94326d4 100644
--- a/config/routes/git_http.rb
+++ b/config/routes/git_http.rb
@@ -1,7 +1,7 @@
scope(path: '*namespace_id/:project_id',
format: nil,
- constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do
- scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do
+ constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
+ scope(constraints: { project_id: Gitlab::PathRegex.project_git_route_regex }, module: :projects) do
# Git HTTP clients ('git clone' etc.)
scope(controller: :git_http) do
get '/info/refs', action: :info_refs
@@ -28,7 +28,7 @@ scope(path: '*namespace_id/:project_id',
end
# Redirect /group/project/info/refs to /group/project.git/info/refs
- scope(constraints: { project_id: Gitlab::Regex.project_route_regex }) do
+ scope(constraints: { project_id: Gitlab::PathRegex.project_route_regex }) do
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 7b29e0e807c..11cdff55ed8 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,9 +1,11 @@
+require 'constraints/group_url_constrainer'
+
resources :groups, only: [:index, :new, :create]
scope(path: 'groups/*group_id',
module: :groups,
as: :group,
- constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
+ constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
@@ -25,7 +27,7 @@ end
scope(path: 'groups/*id',
controller: :groups,
- constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
+ constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
@@ -34,3 +36,15 @@ scope(path: 'groups/*id',
get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical
end
+
+constraints(GroupUrlConstrainer.new) do
+ scope(path: '*id',
+ as: :group,
+ constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ },
+ controller: :groups) do
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
+ end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 07c341999ea..3dc890e5785 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -5,6 +5,7 @@ resource :profile, only: [:show, :update] do
put :reset_private_token
put :reset_incoming_email_token
+ put :reset_rss_token
put :update_username
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 9fe8372edf9..bec1f04d1f9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -13,16 +13,16 @@ constraints(ProjectUrlConstrainer.new) do
# Otherwise, Rails will overwrite the constraint with `/.+?/`,
# which breaks some of our wildcard routes like `/blob/*id`
# and `/tree/*id` that depend on the negative lookahead inside
- # `Gitlab::Regex.namespace_route_regex`, which helps the router
+ # `Gitlab::PathRegex.full_namespace_route_regex`, which helps the router
# determine whether a certain path segment is part of `*namespace_id`,
# `:project_id`, or `*id`.
#
# See https://github.com/rails/rails/blob/v4.2.8/actionpack/lib/action_dispatch/routing/mapper.rb#L155
scope(path: '*namespace_id',
as: :namespace,
- namespace_id: Gitlab::Regex.namespace_route_regex) do
+ namespace_id: Gitlab::PathRegex.full_namespace_route_regex) do
scope(path: ':project_id',
- constraints: { project_id: Gitlab::Regex.project_route_regex },
+ constraints: { project_id: Gitlab::PathRegex.project_route_regex },
module: :projects,
as: :project) do
@@ -216,6 +216,12 @@ constraints(ProjectUrlConstrainer.new) do
member do
get :test
end
+
+ resources :hook_logs, only: [:show] do
+ member do
+ get :retry
+ end
+ end
end
resources :container_registry, only: [:index, :destroy],
@@ -329,7 +335,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
collection do
- scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
constraints format: /svg/ do
get :build
get :coverage
@@ -352,7 +358,7 @@ constraints(ProjectUrlConstrainer.new) do
resources(:projects,
path: '/',
- constraints: { id: Gitlab::Regex.project_route_regex },
+ constraints: { id: Gitlab::PathRegex.project_route_regex },
only: [:edit, :show, :update, :destroy]) do
member do
put :transfer
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 5cf37a06e97..11911636fa7 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -2,7 +2,7 @@
resource :repository, only: [:create] do
member do
- get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
+ get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }
end
end
@@ -24,7 +24,7 @@ scope format: false do
member do
# tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex }
# Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
@@ -34,7 +34,7 @@ scope format: false do
end
end
- scope constraints: { id: Gitlab::Regex.git_reference_regex } do
+ scope constraints: { id: Gitlab::PathRegex.git_reference_regex } do
resources :network, only: [:show]
resources :graphs, only: [:show] do
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 0f3bec9cf58..e682dcd6663 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -11,19 +11,7 @@ devise_scope :user do
get '/users/almost_there' => 'confirmations#almost_there'
end
-constraints(UserUrlConstrainer.new) do
- # Get all keys of user
- get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.root_namespace_route_regex }
-
- scope(path: ':username',
- as: :user,
- constraints: { username: Gitlab::Regex.root_namespace_route_regex },
- controller: :users) do
- get '/', action: :show
- end
-end
-
-scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
+scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do
scope(path: 'users/:username',
as: :user,
controller: :users) do
@@ -34,7 +22,7 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
get :contributed, as: :contributed_projects
get :snippets
get :exists
- get '/', to: redirect('/%{username}')
+ get '/', to: redirect('/%{username}'), as: nil
end
# Compatibility with old routing
@@ -46,3 +34,15 @@ scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do
get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
end
+
+constraints(UserUrlConstrainer.new) do
+ # Get all keys of user
+ get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
+
+ scope(path: ':username',
+ as: :user,
+ constraints: { username: Gitlab::PathRegex.root_namespace_route_regex },
+ controller: :users) do
+ get '/', action: :show
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0ca1f565185..93df2d6f5ff 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -44,9 +44,8 @@
- [project_cache, 1]
- [project_destroy, 1]
- [project_export, 1]
- - [project_web_hook, 1]
+ - [web_hook, 1]
- [repository_check, 1]
- - [system_hook, 1]
- [git_garbage_collect, 1]
- [reactive_caching, 1]
- [cronjob, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 42024739fe9..ce140b75382 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/20170317203554_index_routes_path_for_like.rb b/db/migrate/20170317203554_index_routes_path_for_like.rb
index 7ac09b2abe5..8d3609135d0 100644
--- a/db/migrate/20170317203554_index_routes_path_for_like.rb
+++ b/db/migrate/20170317203554_index_routes_path_for_like.rb
@@ -21,9 +21,8 @@ class IndexRoutesPathForLike < ActiveRecord::Migration
def down
return unless Gitlab::Database.postgresql?
+ return unless index_exists?(:routes, :path, name: INDEX_NAME)
- if index_exists?(:routes, :path, name: INDEX_NAME)
- execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};")
- end
+ remove_concurrent_index_by_name(:routes, INDEX_NAME)
end
end
diff --git a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb
index 9d4380ef960..84635fa39b9 100644
--- a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb
+++ b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb
@@ -11,13 +11,7 @@ class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
disable_ddl_transaction!
def up
- if index_exists? :users, :current_sign_in_at
- if Gitlab::Database.postgresql?
- execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;'
- else
- remove_concurrent_index :users, :current_sign_in_at
- end
- end
+ remove_concurrent_index :users, :current_sign_in_at
end
def down
diff --git a/db/migrate/20170427103502_create_web_hook_logs.rb b/db/migrate/20170427103502_create_web_hook_logs.rb
new file mode 100644
index 00000000000..3643c52180c
--- /dev/null
+++ b/db/migrate/20170427103502_create_web_hook_logs.rb
@@ -0,0 +1,22 @@
+# rubocop:disable all
+class CreateWebHookLogs < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :web_hook_logs do |t|
+ t.references :web_hook, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.string :trigger
+ t.string :url
+ t.text :request_headers
+ t.text :request_data
+ t.text :response_headers
+ t.text :response_body
+ t.string :response_status
+ t.float :execution_duration
+ t.string :internal_error_message
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb b/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb
index 5b8b6c828be..8eb20faa03a 100644
--- a/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb
+++ b/db/migrate/20170503185032_index_redirect_routes_path_for_like.rb
@@ -21,9 +21,8 @@ class IndexRedirectRoutesPathForLike < ActiveRecord::Migration
def down
return unless Gitlab::Database.postgresql?
+ return unless index_exists?(:redirect_routes, :path, name: INDEX_NAME)
- if index_exists?(:redirect_routes, :path, name: INDEX_NAME)
- execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};")
- end
+ remove_concurrent_index_by_name(:redirect_routes, INDEX_NAME)
end
end
diff --git a/db/migrate/20170523091700_add_rss_token_to_users.rb b/db/migrate/20170523091700_add_rss_token_to_users.rb
new file mode 100644
index 00000000000..06a85f6ac3d
--- /dev/null
+++ b/db/migrate/20170523091700_add_rss_token_to_users.rb
@@ -0,0 +1,19 @@
+class AddRssTokenToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :users, :rss_token, :string
+
+ add_concurrent_index :users, :rss_token
+ end
+
+ def down
+ remove_concurrent_index :users, :rss_token if index_exists? :users, :rss_token
+
+ remove_column :users, :rss_token
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 60a95408ac2..43bd50dce90 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1363,6 +1363,7 @@ ActiveRecord::Schema.define(version: 20170524161101) do
t.date "last_activity_on"
t.boolean "notified_of_own_activity"
t.string "preferred_language"
+ t.string "rss_token"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1376,6 +1377,7 @@ ActiveRecord::Schema.define(version: 20170524161101) do
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
+ add_index "users", ["rss_token"], name: "index_users_on_rss_token", using: :btree
add_index "users", ["state"], name: "index_users_on_state", using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
@@ -1390,6 +1392,23 @@ ActiveRecord::Schema.define(version: 20170524161101) do
add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree
add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
+ create_table "web_hook_logs", force: :cascade do |t|
+ t.integer "web_hook_id", null: false
+ t.string "trigger"
+ t.string "url"
+ t.text "request_headers"
+ t.text "request_data"
+ t.text "response_headers"
+ t.text "response_body"
+ t.string "response_status"
+ t.float "execution_duration"
+ t.string "internal_error_message"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "web_hook_logs", ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree
+
create_table "web_hooks", force: :cascade do |t|
t.string "url", limit: 2000
t.integer "project_id"
@@ -1453,4 +1472,5 @@ ActiveRecord::Schema.define(version: 20170524161101) do
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
-end
+ add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
+end \ No newline at end of file
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index 44eca68aaca..735345bd126 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -7,6 +7,14 @@ For working with internationalization (i18n) we use
tool for this task and we have a lot of applications that will help us to work
with it.
+## Setting up GitLab Development Kit (GDK)
+
+In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) project we must download and
+configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit), we can do it by following this [guide](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md).
+
+Once we have the GitLab project ready we can start working on the
+translation of the project.
+
## Tools
We use a couple of gems:
@@ -211,9 +219,11 @@ Let's suppose you want to add translations for a new language, let's say French.
you just need to separate the region with an underscore (`_`). For example:
```sh
- bundle exec rake gettext:add_language[en_gb]
+ bundle exec rake gettext:add_language[en_GB]
```
+ Please note that you need to specify the region part in capitals.
+
1. Now that the language is added, a new directory has been created under the
path: `locale/fr/`. You can now start using your PO editor to edit the PO file
located in: `locale/fr/gitlab.edit.po`.
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/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 39ff4f8c1b8..b4ffd57afbb 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -206,9 +206,43 @@ its class in an annotation.
>**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
-Setting up an Ingress controller can be as simple as installing the `nginx-ingress` helm chart. But be sure
+Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md)
+#### Preserving Source IPs
+
+If you are using the `LoadBalancer` serviceType you may run into issues where user IP addresses in the GitLab
+logs, and used in abuse throttling are not accurate. This is due to how Kubernetes uses source NATing on cluster nodes without endpoints.
+
+See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) for more information.
+
+To fix this you can add the following service annotation to your `values.yaml`
+
+```yaml
+## For minikube, set this to NodePort, elsewhere use LoadBalancer
+## ref: http://kubernetes.io/docs/user-guide/services/#publishing-services---service-types
+##
+serviceType: LoadBalancer
+
+## Optional annotations for gitlab service.
+serviceAnnotations:
+ service.beta.kubernetes.io/external-traffic: "OnlyLocal"
+```
+
+>**Note:**
+If you are using the ingress routing, you will likely also need to specify the annotation on the service for the ingress
+controller. For `nginx-ingress` you can check the
+[configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration)
+on how to add the annotation to the `controller.service.annotations` array.
+
+>**Note:**
+When using the `nginx-ingress` controller on Google Container Engine (GKE), and using the `external-traffic` annotation,
+you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node
+as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab.
+See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and
+[nginx-ingress configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration)
+for more information.
+
### External database
You can configure the GitLab Helm chart to connect to an external PostgreSQL
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 151c17f3bf1..d5edf36f6b0 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -71,7 +71,7 @@ structure.
- You need to be an Owner of a group in order to be able to create
a subgroup. For more information check the [permissions table][permissions].
- For a list of words that are not allowed to be used as group names see the
- [`regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
+ [`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
- `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
- `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
- `GROUP_ROUTES`: are names that are reserved for all groups or projects.
@@ -163,4 +163,4 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group
-[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/regex.rb
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb
diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png
new file mode 100755
index 00000000000..917068d9398
--- /dev/null
+++ b/doc/user/project/integrations/img/webhook_logs.png
Binary files differ
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/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 48d49c5d40c..d0bb1cd11a8 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1017,6 +1017,22 @@ X-Gitlab-Event: Build Hook
}
```
+## Troubleshoot webhooks
+
+Gitlab stores each perform of the webhook.
+You can find records for last 2 days in "Recent Deliveries" section on the edit page of each webhook.
+
+![Recent deliveries](img/webhook_logs.png)
+
+In this section you can see HTTP status code (green for 200-299 codes, red for the others, `internal error` for failed deliveries ), triggered event, a time when the event was called, elapsed time of the request.
+
+If you need more information about execution, you can click `View details` link.
+On this page, you can see data that GitLab sends (request headers and body) and data that it received (response headers and body).
+
+From this page, you can repeat delivery with the same data by clicking `Resend Request` button.
+
+>**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address.
+
## Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/user/project/milestones/img/progress.png b/doc/user/project/milestones/img/progress.png
new file mode 100644
index 00000000000..c85aecca729
--- /dev/null
+++ b/doc/user/project/milestones/img/progress.png
Binary files differ
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index a43a42a8fe8..99233ed5ae2 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -44,3 +44,11 @@ special options available when filtering by milestone:
* **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several
milestones in the same project.
+
+## Milestone progress statistics
+
+Milestone statistics can be viewed in the milestone sidebar. The milestone percentage statistic
+is calculated as; closed and merged merge requests plus all closed issues divided by
+total merge requests and issues.
+
+![Milestone statistics](img/progress.png)
diff --git a/features/project/hooks.feature b/features/project/hooks.feature
deleted file mode 100644
index 627738004c4..00000000000
--- a/features/project/hooks.feature
+++ /dev/null
@@ -1,37 +0,0 @@
-Feature: Project Hooks
- Background:
- Given I sign in as a user
- And I own project "Shop"
-
- Scenario: I should see hook list
- Given project has hook
- When I visit project hooks page
- Then I should see project hook
-
- Scenario: I add new hook
- Given I visit project hooks page
- When I submit new hook
- Then I should see newly created hook
-
- Scenario: I add new hook with SSL verification enabled
- Given I visit project hooks page
- When I submit new hook with SSL verification enabled
- Then I should see newly created hook with SSL verification enabled
-
- Scenario: I test hook
- Given project has hook
- And I visit project hooks page
- When I click test hook button
- Then hook should be triggered
-
- Scenario: I test a hook on empty project
- Given I own empty project with hook
- And I visit project hooks page
- When I click test hook button
- Then I should see hook error message
-
- Scenario: I test a hook on down URL
- Given project has hook
- And I visit project hooks page
- When I click test hook button with invalid URL
- Then I should see hook service down error message
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/hooks.rb b/features/steps/project/hooks.rb
deleted file mode 100644
index 945d58a6458..00000000000
--- a/features/steps/project/hooks.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require 'webmock'
-
-class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include RSpec::Matchers
- include RSpec::Mocks::ExampleMethods
- include WebMock::API
-
- step 'project has hook' do
- @hook = create(:project_hook, project: current_project)
- end
-
- step 'I own empty project with hook' do
- @project = create(:empty_project,
- name: 'Empty Project', namespace: @user.namespace)
- @hook = create(:project_hook, project: current_project)
- end
-
- step 'I should see project hook' do
- expect(page).to have_content @hook.url
- end
-
- step 'I submit new hook' do
- @url = 'http://example.org/1'
- fill_in "hook_url", with: @url
- expect { click_button "Add webhook" }.to change(ProjectHook, :count).by(1)
- end
-
- step 'I submit new hook with SSL verification enabled' do
- @url = 'http://example.org/2'
- fill_in "hook_url", with: @url
- check "hook_enable_ssl_verification"
- expect { click_button "Add webhook" }.to change(ProjectHook, :count).by(1)
- end
-
- step 'I should see newly created hook' do
- expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
- expect(page).to have_content(@url)
- end
-
- step 'I should see newly created hook with SSL verification enabled' do
- expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
- expect(page).to have_content(@url)
- expect(page).to have_content("SSL Verification: enabled")
- end
-
- step 'I click test hook button' do
- stub_request(:post, @hook.url).to_return(status: 200)
- click_link 'Test'
- end
-
- step 'I click test hook button with invalid URL' do
- stub_request(:post, @hook.url).to_raise(SocketError)
- click_link 'Test'
- end
-
- step 'hook should be triggered' do
- expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
- expect(page).to have_selector '.flash-notice',
- text: 'Hook executed successfully: HTTP 200'
- end
-
- step 'I should see hook error message' do
- expect(page).to have_selector '.flash-alert',
- text: 'Hook execution failed. '\
- 'Ensure the project has commits.'
- end
-
- step 'I should see hook service down error message' do
- expect(page).to have_selector '.flash-alert',
- text: 'Hook execution failed: Exception from'
- end
-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/api.rb b/lib/api/api.rb
index 52cd7cbe3db..ac113c5200d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -45,9 +45,9 @@ module API
end
before { allow_access_with_scope :api }
- before { Gitlab::I18n.set_locale(current_user) }
+ before { Gitlab::I18n.locale = current_user&.preferred_language }
- after { Gitlab::I18n.reset_locale }
+ after { Gitlab::I18n.use_default_locale }
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b5308aeecf6..936f3283877 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -252,7 +252,9 @@ module API
class RepoDiff < Grape::Entity
expose :old_path, :new_path, :a_mode, :b_mode, :diff
- expose :new_file, :renamed_file, :deleted_file
+ expose :new_file?, as: :new_file
+ expose :renamed_file?, as: :renamed_file
+ expose :deleted_file?, as: :deleted_file
end
class Milestone < ProjectEntity
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 8f16e532ecb..14d2bff9cb5 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -85,7 +85,7 @@ module API
optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
optional :format, type: String, desc: 'The archive format'
end
- get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
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/repositories.rb b/lib/api/v3/repositories.rb
index e4d14bc8168..0eaa0de2eef 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -72,7 +72,7 @@ module API
optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
optional :format, type: String, desc: 'The archive format'
end
- get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index 0ea2f97352d..6fc1d56d7a0 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -1,9 +1,9 @@
class GroupUrlConstrainer
def matches?(request)
- id = request.params[:group_id] || request.params[:id]
+ full_path = request.params[:group_id] || request.params[:id]
- return false unless DynamicPathValidator.valid_namespace_path?(id)
+ return false unless DynamicPathValidator.valid_group_path?(full_path)
- Group.find_by_full_path(id, follow_redirects: request.get?).present?
+ Group.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 4444a1abee3..4c0aee6c48f 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -2,7 +2,7 @@ class ProjectUrlConstrainer
def matches?(request)
namespace_path = request.params[:namespace_id]
project_path = request.params[:project_id] || request.params[:id]
- full_path = namespace_path + '/' + project_path
+ full_path = [namespace_path, project_path].join('/')
return false unless DynamicPathValidator.valid_project_path?(full_path)
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
index 28159dc0dec..d16ae7f3f40 100644
--- a/lib/constraints/user_url_constrainer.rb
+++ b/lib/constraints/user_url_constrainer.rb
@@ -1,5 +1,9 @@
class UserUrlConstrainer
def matches?(request)
- User.find_by_full_path(request.params[:username], follow_redirects: request.get?).present?
+ full_path = request.params[:username]
+
+ return false unless DynamicPathValidator.valid_user_path?(full_path)
+
+ User.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
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/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index e76c9abbe04..a412bb6dbd2 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -42,7 +42,7 @@ module Gitlab
'in the body of your migration class'
end
- if Database.postgresql?
+ if supports_drop_index_concurrently?
options = options.merge({ algorithm: :concurrently })
disable_statement_timeout
end
@@ -50,6 +50,39 @@ module Gitlab
remove_index(table_name, options.merge({ column: column_name }))
end
+ # Removes an existing index, concurrently when supported
+ #
+ # On PostgreSQL this method removes an index concurrently.
+ #
+ # Example:
+ #
+ # remove_concurrent_index :users, "index_X_by_Y"
+ #
+ # See Rails' `remove_index` for more info on the available arguments.
+ def remove_concurrent_index_by_name(table_name, index_name, options = {})
+ if transaction_open?
+ raise 'remove_concurrent_index_by_name can not be run inside a transaction, ' \
+ 'you can disable transactions by calling disable_ddl_transaction! ' \
+ 'in the body of your migration class'
+ end
+
+ if supports_drop_index_concurrently?
+ options = options.merge({ algorithm: :concurrently })
+ disable_statement_timeout
+ end
+
+ remove_index(table_name, options.merge({ name: index_name }))
+ end
+
+ # Only available on Postgresql >= 9.2
+ def supports_drop_index_concurrently?
+ return false unless Database.postgresql?
+
+ version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
+
+ version >= 90200
+ end
+
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking when using PostgreSQL. When
diff --git a/lib/gitlab/dependency_linker.rb b/lib/gitlab/dependency_linker.rb
index c45ae8feb2c..3192bf6f667 100644
--- a/lib/gitlab/dependency_linker.rb
+++ b/lib/gitlab/dependency_linker.rb
@@ -1,7 +1,16 @@
module Gitlab
module DependencyLinker
LINKERS = [
- GemfileLinker
+ GemfileLinker,
+ GemspecLinker,
+ PackageJsonLinker,
+ ComposerJsonLinker,
+ PodfileLinker,
+ PodspecLinker,
+ PodspecJsonLinker,
+ CartfileLinker,
+ GodepsJsonLinker,
+ RequirementsTxtLinker
].freeze
def self.linker(blob_name)
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
index 5f4027e7e81..7bbd154eb03 100644
--- a/lib/gitlab/dependency_linker/base_linker.rb
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -1,6 +1,9 @@
module Gitlab
module DependencyLinker
class BaseLinker
+ URL_REGEX = %r{https?://[^'" ]+}.freeze
+ REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
+
class_attribute :file_type
def self.support?(blob_name)
@@ -26,59 +29,20 @@ module Gitlab
private
- def package_url(name)
- raise NotImplementedError
- end
-
def link_dependencies
raise NotImplementedError
end
- def package_link(name, url = package_url(name))
- return name unless url
-
- %{<a href="#{ERB::Util.html_escape_once(url)}" rel="noopener noreferrer" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}
+ def license_url(name)
+ Licensee::License.find(name)&.url
end
- # Links package names in a method call or assignment string argument.
- #
- # Example:
- # link_method_call("gem")
- # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"`
- #
- # link_method_call("gem", "specific_package")
- # # Will link `specific_package` in `gem "specific_package"`
- #
- # link_method_call("github", /[^\/]+\/[^\/]+/)
- # # Will link `user/repo` in `github "user/repo"`, but not `github "package"`
- #
- # link_method_call(%w[add_dependency add_development_dependency])
- # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"`
- #
- # link_method_call("name")
- # # Will link `package` in `self.name = "package"`
- def link_method_call(method_names, value = nil, &url_proc)
- value =
- case value
- when String
- Regexp.escape(value)
- when nil
- /[^'"]+/
- else
- value
- end
-
- method_names = Array(method_names).map { |name| Regexp.escape(name) }
-
- regex = %r{
- #{Regexp.union(method_names)} # Method name
- \s* # Whitespace
- [(=]? # Opening brace or equals sign
- \s* # Whitespace
- ['"](?<name>#{value})['"] # Package name in quotes
- }x
+ def github_url(name)
+ "https://github.com/#{name}"
+ end
- link_regex(regex, &url_proc)
+ def link_tag(name, url)
+ %{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}
end
# Links package names based on regex.
@@ -86,13 +50,13 @@ module Gitlab
# Example:
# link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/)
# # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
- def link_regex(regex)
+ def link_regex(regex, &url_proc)
highlighted_lines.map!.with_index do |rich_line, i|
marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe)
marker.mark(regex, group: :name) do |text, left:, right:|
- url = block_given? ? yield(text) : package_url(text)
- package_link(text, url)
+ url = yield(text)
+ url ? link_tag(text, url) : text
end
end
end
@@ -104,6 +68,19 @@ module Gitlab
def highlighted_lines
@highlighted_lines ||= highlighted_text.lines
end
+
+ def regexp_for_value(value, default: /[^'" ]+/)
+ case value
+ when Array
+ Regexp.union(value.map { |v| regexp_for_value(v, default: default) })
+ when String
+ Regexp.escape(value)
+ when Regexp
+ value
+ else
+ default
+ end
+ end
end
end
end
diff --git a/lib/gitlab/dependency_linker/cartfile_linker.rb b/lib/gitlab/dependency_linker/cartfile_linker.rb
new file mode 100644
index 00000000000..4f69f2c4ab2
--- /dev/null
+++ b/lib/gitlab/dependency_linker/cartfile_linker.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module DependencyLinker
+ class CartfileLinker < MethodLinker
+ self.file_type = :cartfile
+
+ private
+
+ def link_dependencies
+ link_method_call('github', REPO_REGEX, &method(:github_url))
+ link_method_call(%w[github git binary], URL_REGEX, &:itself)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/cocoapods.rb b/lib/gitlab/dependency_linker/cocoapods.rb
new file mode 100644
index 00000000000..2fbde7da1b4
--- /dev/null
+++ b/lib/gitlab/dependency_linker/cocoapods.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module DependencyLinker
+ module Cocoapods
+ def package_url(name)
+ package = name.split("/", 2).first
+ "https://cocoapods.org/pods/#{package}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb
new file mode 100644
index 00000000000..0245bf4077a
--- /dev/null
+++ b/lib/gitlab/dependency_linker/composer_json_linker.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module DependencyLinker
+ class ComposerJsonLinker < PackageJsonLinker
+ self.file_type = :composer_json
+
+ private
+
+ def link_packages
+ link_packages_at_key("require", &method(:package_url))
+ link_packages_at_key("require-dev", &method(:package_url))
+ end
+
+ def package_url(name)
+ "https://packagist.org/packages/#{name}" if name =~ %r{\A#{REPO_REGEX}\z}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/gemfile_linker.rb b/lib/gitlab/dependency_linker/gemfile_linker.rb
index 9b82e126528..d034ea67387 100644
--- a/lib/gitlab/dependency_linker/gemfile_linker.rb
+++ b/lib/gitlab/dependency_linker/gemfile_linker.rb
@@ -1,28 +1,31 @@
module Gitlab
module DependencyLinker
- class GemfileLinker < BaseLinker
+ class GemfileLinker < MethodLinker
self.file_type = :gemfile
private
def link_dependencies
- # Link `gem "package_name"` to https://rubygems.org/gems/package_name
- link_method_call("gem")
+ link_urls
+ link_packages
+ end
+ def link_urls
# Link `github: "user/repo"` to https://github.com/user/repo
- link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/) do |name|
- "https://github.com/#{name}"
- end
+ link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url))
# Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
- link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>https?://[^'"]+)['"]}) { |url| url }
+ link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself)
# Link `source "https://rubygems.org"` to https://rubygems.org
- link_method_call("source", %r{https?://[^'"]+}) { |url| url }
+ link_method_call('source', URL_REGEX, &:itself)
end
- def package_url(name)
- "https://rubygems.org/gems/#{name}"
+ def link_packages
+ # Link `gem "package_name"` to https://rubygems.org/gems/package_name
+ link_method_call('gem') do |name|
+ "https://rubygems.org/gems/#{name}"
+ end
end
end
end
diff --git a/lib/gitlab/dependency_linker/gemspec_linker.rb b/lib/gitlab/dependency_linker/gemspec_linker.rb
new file mode 100644
index 00000000000..f1783ee2ab4
--- /dev/null
+++ b/lib/gitlab/dependency_linker/gemspec_linker.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module DependencyLinker
+ class GemspecLinker < MethodLinker
+ self.file_type = :gemspec
+
+ private
+
+ def link_dependencies
+ link_method_call('homepage', URL_REGEX, &:itself)
+ link_method_call('license', &method(:license_url))
+
+ link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name|
+ "https://rubygems.org/gems/#{name}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb
new file mode 100644
index 00000000000..fe091baee6d
--- /dev/null
+++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module DependencyLinker
+ class GodepsJsonLinker < JsonLinker
+ NESTED_REPO_REGEX = %r{([^/]+/)+[^/]+?}.freeze
+
+ self.file_type = :godeps_json
+
+ private
+
+ def link_dependencies
+ link_json('ImportPath') do |path|
+ case path
+ when %r{\A(?<repo>gitlab\.com/#{NESTED_REPO_REGEX})\.git/(?<path>.+)\z},
+ %r{\A(?<repo>git(lab|hub)\.com/#{REPO_REGEX})/(?<path>.+)\z}
+
+ "https://#{$~[:repo]}/tree/master/#{$~[:path]}"
+ when /\Agolang\.org/
+ "https://godoc.org/#{path}"
+ else
+ "https://#{path}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb
new file mode 100644
index 00000000000..a8ef25233d8
--- /dev/null
+++ b/lib/gitlab/dependency_linker/json_linker.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module DependencyLinker
+ class JsonLinker < BaseLinker
+ def link
+ return highlighted_text unless json
+
+ super
+ end
+
+ private
+
+ # Links package names in a JSON key or values.
+ #
+ # Example:
+ # link_json('name')
+ # # Will link `package` in `"name": "package"`
+ #
+ # link_json('name', 'specific_package')
+ # # Will link `specific_package` in `"name": "specific_package"`
+ #
+ # link_json('name', /[^\/]+\/[^\/]+/)
+ # # Will link `user/repo` in `"name": "user/repo"`, but not `"name": "package"`
+ #
+ # 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: /[^" ]+/)
+
+ if link == :value
+ value = /(?<name>#{value})/
+ else
+ key = /(?<name>#{key})/
+ end
+
+ link_regex(/"#{key}":\s*"#{value}"/, &url_proc)
+ end
+
+ def json
+ @json ||= JSON.parse(plain_text) rescue nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/method_linker.rb b/lib/gitlab/dependency_linker/method_linker.rb
new file mode 100644
index 00000000000..0ffa2a83c93
--- /dev/null
+++ b/lib/gitlab/dependency_linker/method_linker.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module DependencyLinker
+ class MethodLinker < BaseLinker
+ private
+
+ # Links package names in a method call or assignment string argument.
+ #
+ # Example:
+ # link_method_call('gem')
+ # # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"`
+ #
+ # link_method_call('gem', 'specific_package')
+ # # Will link `specific_package` in `gem "specific_package"`
+ #
+ # link_method_call('github', /[^\/"]+\/[^\/"]+/)
+ # # Will link `user/repo` in `github "user/repo"`, but not `github "package"`
+ #
+ # link_method_call(%w[add_dependency add_development_dependency])
+ # # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"`
+ #
+ # link_method_call('name')
+ # # Will link `package` in `self.name = "package"`
+ def link_method_call(method_name, value = nil, &url_proc)
+ method_name = regexp_for_value(method_name)
+ value = regexp_for_value(value)
+
+ regex = %r{
+ #{method_name} # Method name
+ \s* # Whitespace
+ [(=]? # Opening brace or equals sign
+ \s* # Whitespace
+ ['"](?<name>#{value})['"] # Package name in quotes
+ }x
+
+ link_regex(regex, &url_proc)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/package_json_linker.rb b/lib/gitlab/dependency_linker/package_json_linker.rb
new file mode 100644
index 00000000000..330c95f0880
--- /dev/null
+++ b/lib/gitlab/dependency_linker/package_json_linker.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module DependencyLinker
+ class PackageJsonLinker < JsonLinker
+ self.file_type = :package_json
+
+ private
+
+ def link_dependencies
+ link_json('name', json["name"], &method(:package_url))
+ link_json('license', &method(:license_url))
+ link_json(%w[homepage url], URL_REGEX, &:itself)
+
+ link_packages
+ end
+
+ def link_packages
+ link_packages_at_key("dependencies", &method(:package_url))
+ link_packages_at_key("devDependencies", &method(:package_url))
+ end
+
+ def link_packages_at_key(key, &url_proc)
+ dependencies = json[key]
+ return unless dependencies
+
+ dependencies.each do |name, version|
+ link_json(name, version, link: :key, &url_proc)
+
+ link_json(name) do |value|
+ case value
+ when /\A#{URL_REGEX}\z/
+ value
+ when /\A#{REPO_REGEX}\z/
+ github_url(value)
+ end
+ end
+ end
+ end
+
+ def package_url(name)
+ "https://npmjs.com/package/#{name}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/podfile_linker.rb b/lib/gitlab/dependency_linker/podfile_linker.rb
new file mode 100644
index 00000000000..60ad166ea17
--- /dev/null
+++ b/lib/gitlab/dependency_linker/podfile_linker.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module DependencyLinker
+ class PodfileLinker < GemfileLinker
+ include Cocoapods
+
+ self.file_type = :podfile
+
+ private
+
+ def link_packages
+ link_method_call('pod', &method(:package_url))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/podspec_json_linker.rb b/lib/gitlab/dependency_linker/podspec_json_linker.rb
new file mode 100644
index 00000000000..d82237ed3f1
--- /dev/null
+++ b/lib/gitlab/dependency_linker/podspec_json_linker.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module DependencyLinker
+ class PodspecJsonLinker < JsonLinker
+ include Cocoapods
+
+ self.file_type = :podspec_json
+
+ private
+
+ def link_dependencies
+ link_json('name', json["name"], &method(:package_url))
+ link_json('license', &method(:license_url))
+ link_json(%w[homepage git], URL_REGEX, &:itself)
+
+ link_packages_at_key("dependencies", &method(:package_url))
+
+ json["subspecs"]&.each do |subspec|
+ link_packages_at_key("dependencies", subspec, &method(:package_url))
+ end
+ end
+
+ def link_packages_at_key(key, root = json, &url_proc)
+ dependencies = root[key]
+ return unless dependencies
+
+ dependencies.each do |name, _|
+ link_regex(/"(?<name>#{Regexp.escape(name)})":\s*\[/, &url_proc)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/podspec_linker.rb b/lib/gitlab/dependency_linker/podspec_linker.rb
new file mode 100644
index 00000000000..a52c7a02439
--- /dev/null
+++ b/lib/gitlab/dependency_linker/podspec_linker.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module DependencyLinker
+ class PodspecLinker < MethodLinker
+ include Cocoapods
+
+ STRING_REGEX = /['"](?<name>[^'"]+)['"]/.freeze
+
+ self.file_type = :podspec
+
+ private
+
+ def link_dependencies
+ link_method_call('homepage', URL_REGEX, &:itself)
+
+ link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself)
+
+ link_method_call('license', &method(:license_url))
+ link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url))
+
+ link_method_call(%w[name dependency], &method(:package_url))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
new file mode 100644
index 00000000000..2e197e5cd94
--- /dev/null
+++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module DependencyLinker
+ class RequirementsTxtLinker < BaseLinker
+ self.file_type = :requirements_txt
+
+ private
+
+ def link_dependencies
+ link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) do |name|
+ "https://pypi.python.org/pypi/#{name}"
+ end
+
+ link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index c6bf25b5874..2aef7fdaa35 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -1,16 +1,17 @@
module Gitlab
module Diff
class File
- attr_reader :diff, :repository, :diff_refs
+ attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs
- delegate :new_file, :deleted_file, :renamed_file,
- :old_path, :new_path, :a_mode, :b_mode,
+ delegate :new_file?, :deleted_file?, :renamed_file?,
+ :old_path, :new_path, :a_mode, :b_mode, :mode_changed?,
:submodule?, :too_large?, :collapsed?, to: :diff, prefix: false
- def initialize(diff, repository:, diff_refs: nil)
+ def initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil)
@diff = diff
@repository = repository
@diff_refs = diff_refs
+ @fallback_diff_refs = fallback_diff_refs
end
def position(line)
@@ -49,24 +50,60 @@ module Gitlab
line_code(line) if line
end
+ def old_sha
+ diff_refs&.base_sha
+ end
+
+ def new_sha
+ diff_refs&.head_sha
+ end
+
+ def content_sha
+ return old_content_sha if deleted_file?
+ return @content_sha if defined?(@content_sha)
+
+ refs = diff_refs || fallback_diff_refs
+ @content_sha = refs&.head_sha
+ end
+
def content_commit
- return unless diff_refs
+ return @content_commit if defined?(@content_commit)
+
+ sha = content_sha
+ @content_commit = repository.commit(sha) if sha
+ end
+
+ def old_content_sha
+ return if new_file?
+ return @old_content_sha if defined?(@old_content_sha)
- repository.commit(deleted_file ? old_ref : new_ref)
+ refs = diff_refs || fallback_diff_refs
+ @old_content_sha = refs&.base_sha
end
def old_content_commit
- return unless diff_refs
+ return @old_content_commit if defined?(@old_content_commit)
- repository.commit(old_ref)
+ sha = old_content_sha
+ @old_content_commit = repository.commit(sha) if sha
end
- def old_ref
- diff_refs.try(:base_sha)
+ def blob
+ return @blob if defined?(@blob)
+
+ sha = content_sha
+ return @blob = nil unless sha
+
+ repository.blob_at(sha, file_path)
end
- def new_ref
- diff_refs.try(:head_sha)
+ def old_blob
+ return @old_blob if defined?(@old_blob)
+
+ sha = old_content_sha
+ return @old_blob = nil unless sha
+
+ @old_blob = repository.blob_at(sha, old_path)
end
attr_writer :highlighted_diff_lines
@@ -85,10 +122,6 @@ module Gitlab
@parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end
- def mode_changed?
- a_mode && b_mode && a_mode != b_mode
- end
-
def raw_diff
diff.diff.to_s
end
@@ -117,20 +150,8 @@ module Gitlab
diff_lines.count(&:removed?)
end
- def old_blob(commit = old_content_commit)
- return unless commit
-
- repository.blob_at(commit.id, old_path)
- end
-
- def blob(commit = content_commit)
- return unless commit
-
- repository.blob_at(commit.id, file_path)
- end
-
def file_identifier
- "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
+ "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
end
end
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 7c32adc6ce7..79836a2fbab 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -2,7 +2,7 @@ module Gitlab
module Diff
module FileCollection
class Base
- attr_reader :project, :diff_options, :diff_view, :diff_refs
+ attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs
delegate :count, :size, :real_size, to: :diff_files
@@ -10,14 +10,15 @@ module Gitlab
::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false)
end
- def initialize(diffable, project:, diff_options: nil, diff_refs: nil)
+ def initialize(diffable, project:, diff_options: nil, diff_refs: nil, fallback_diff_refs: nil)
diff_options = self.class.default_options.merge(diff_options || {})
- @diffable = diffable
- @diffs = diffable.raw_diffs(diff_options)
- @project = project
+ @diffable = diffable
+ @diffs = diffable.raw_diffs(diff_options)
+ @project = project
@diff_options = diff_options
- @diff_refs = diff_refs
+ @diff_refs = diff_refs
+ @fallback_diff_refs = fallback_diff_refs
end
def diff_files
@@ -35,7 +36,7 @@ module Gitlab
private
def decorate_diff!(diff)
- Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs)
+ Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs, fallback_diff_refs: fallback_diff_refs)
end
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 0bd226ef050..9a58b500a2c 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -8,7 +8,8 @@ module Gitlab
super(merge_request_diff,
project: merge_request_diff.project,
diff_options: diff_options,
- diff_refs: merge_request_diff.diff_refs)
+ diff_refs: merge_request_diff.diff_refs,
+ fallback_diff_refs: merge_request_diff.fallback_diff_refs)
end
def diff_files
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 7db896522a9..ed2f541977a 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -3,7 +3,7 @@ module Gitlab
class Highlight
attr_reader :diff_file, :diff_lines, :raw_lines, :repository
- delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff
+ delegate :old_path, :new_path, :old_sha, :new_sha, to: :diff_file, prefix: :diff
def initialize(diff_lines, repository: nil)
@repository = repository
@@ -61,12 +61,12 @@ module Gitlab
def old_lines
return unless diff_file
- @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_ref, diff_old_path)
+ @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_sha, diff_old_path)
end
def new_lines
return unless diff_file
- @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_ref, diff_new_path)
+ @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_sha, diff_new_path)
end
end
end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 2b0e19b338b..cc285162b44 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -10,7 +10,7 @@ module Gitlab
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
- RESERVED_WORDS = Gitlab::Regex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
+ RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 31d1b66b4f7..deade337354 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -11,6 +11,10 @@ module Gitlab
# Stats properties
attr_accessor :new_file, :renamed_file, :deleted_file
+ alias_method :new_file?, :new_file
+ alias_method :deleted_file?, :deleted_file
+ alias_method :renamed_file?, :renamed_file
+
attr_accessor :too_large
# The maximum size of a diff to display.
@@ -208,6 +212,10 @@ module Gitlab
hash
end
+ def mode_changed?
+ a_mode && b_mode && a_mode != b_mode
+ end
+
def submodule?
a_mode == '160000' || b_mode == '160000'
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index bcbad8ec829..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,17 +57,17 @@ module Gitlab
collection = each_with_index do |element, i|
@array[i] = yield(element)
end
- @populated = true
collection
end
+ alias_method :to_ary, :to_a
+
private
def populate!
return if @populated
each { nil } # force a loop through all diffs
- @populated = true
nil
end
@@ -79,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
@@ -113,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/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/i18n.rb b/lib/gitlab/i18n.rb
index 3411516319f..5ab3eeb3aff 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -12,15 +12,36 @@ module Gitlab
AVAILABLE_LANGUAGES.keys
end
- def set_locale(current_user)
- requested_locale = current_user&.preferred_language || ::I18n.default_locale
- locale = FastGettext.set_locale(requested_locale)
- ::I18n.locale = locale
+ def locale
+ FastGettext.locale
end
- def reset_locale
+ def locale=(locale_string)
+ requested_locale = locale_string || ::I18n.default_locale
+ new_locale = FastGettext.set_locale(requested_locale)
+ ::I18n.locale = new_locale
+ end
+
+ def use_default_locale
FastGettext.set_locale(::I18n.default_locale)
::I18n.locale = ::I18n.default_locale
end
+
+ def with_locale(locale_string)
+ original_locale = locale
+
+ self.locale = locale_string
+ yield
+ ensure
+ self.locale = original_locale
+ end
+
+ def with_user_locale(user, &block)
+ with_locale(user&.preferred_language, &block)
+ end
+
+ def with_default_locale(&block)
+ with_locale(::I18n.default_locale, &block)
+ end
end
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
new file mode 100644
index 00000000000..1c0abc9f7cf
--- /dev/null
+++ b/lib/gitlab/path_regex.rb
@@ -0,0 +1,264 @@
+module Gitlab
+ module PathRegex
+ extend self
+
+ # All routes that appear on the top level must be listed here.
+ # This will make sure that groups cannot be created with these names
+ # as these routes would be masked by the paths already in place.
+ #
+ # Example:
+ # /api/api-project
+ #
+ # the path `api` shouldn't be allowed because it would be masked by `api/*`
+ #
+ TOP_LEVEL_ROUTES = %w[
+ -
+ .well-known
+ abuse_reports
+ admin
+ all
+ api
+ assets
+ autocomplete
+ ci
+ dashboard
+ explore
+ files
+ groups
+ health_check
+ help
+ hooks
+ import
+ invites
+ issues
+ jwt
+ koding
+ member
+ merge_requests
+ new
+ notes
+ notification_settings
+ oauth
+ profile
+ projects
+ public
+ repository
+ robots.txt
+ s
+ search
+ sent_notifications
+ services
+ snippets
+ teams
+ u
+ unicorn_test
+ unsubscribes
+ uploads
+ users
+ ].freeze
+
+ # This list should contain all words following `/*namespace_id/:project_id` in
+ # routes that contain a second wildcard.
+ #
+ # Example:
+ # /*namespace_id/:project_id/badges/*ref/build
+ #
+ # If `badges` was allowed as a project/group name, we would not be able to access the
+ # `badges` route for those projects:
+ #
+ # Consider a namespace with path `foo/bar` and a project called `badges`.
+ # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
+ #
+ # When accessing this path the route would be matched to the `badges` path
+ # with the following params:
+ # - namespace_id: `foo`
+ # - project_id: `bar`
+ # - ref: `badges/master`
+ #
+ # Failing to find the project, this would result in a 404.
+ #
+ # By rejecting `badges` the router can _count_ on the fact that `badges` will
+ # be preceded by the `namespace/project`.
+ PROJECT_WILDCARD_ROUTES = %w[
+ badges
+ blame
+ blob
+ builds
+ commits
+ create
+ create_dir
+ edit
+ environments/folders
+ files
+ find_file
+ gitlab-lfs/objects
+ info/lfs/objects
+ new
+ preview
+ raw
+ refs
+ tree
+ update
+ wikis
+ ].freeze
+
+ # These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
+ # We need to reject these because we have a `/groups/*id` page that is the same
+ # as the `/*id`.
+ #
+ # If we would allow a subgroup to be created with the name `activity` then
+ # this group would not be accessible through `/groups/parent/activity` since
+ # this would map to the activity-page of its parent.
+ GROUP_ROUTES = %w[
+ activity
+ analytics
+ audit_events
+ avatar
+ edit
+ group_members
+ hooks
+ issues
+ labels
+ ldap
+ ldap_group_links
+ merge_requests
+ milestones
+ notification_setting
+ pipeline_quota
+ projects
+ subgroups
+ ].freeze
+
+ ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
+ ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
+
+ # The namespace regex is used in JavaScript to validate usernames in the "Register" form. However, Javascript
+ # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
+ # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
+ # allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
+ # `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
+ # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
+ PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
+ NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+
+ NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
+ NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze
+ PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze
+ FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/)*#{NAMESPACE_FORMAT_REGEX}}.freeze
+
+ def root_namespace_route_regex
+ @root_namespace_route_regex ||= begin
+ illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE)
+
+ single_line_regexp %r{
+ (?!(#{illegal_words})/)
+ #{NAMESPACE_FORMAT_REGEX}
+ }x
+ end
+ end
+
+ def full_namespace_route_regex
+ @full_namespace_route_regex ||= begin
+ illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE)
+
+ single_line_regexp %r{
+ #{root_namespace_route_regex}
+ (?:
+ /
+ (?!#{illegal_words}/)
+ #{NAMESPACE_FORMAT_REGEX}
+ )*
+ }x
+ end
+ end
+
+ def project_route_regex
+ @project_route_regex ||= begin
+ illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE)
+
+ single_line_regexp %r{
+ (?!(#{illegal_words})/)
+ #{PROJECT_PATH_FORMAT_REGEX}
+ }x
+ end
+ end
+
+ def project_git_route_regex
+ @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
+ end
+
+ def root_namespace_path_regex
+ @root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z}
+ end
+
+ def full_namespace_path_regex
+ @full_namespace_path_regex ||= %r{\A#{full_namespace_route_regex}/\z}
+ end
+
+ def project_path_regex
+ @project_path_regex ||= %r{\A#{project_route_regex}/\z}
+ end
+
+ def full_project_path_regex
+ @full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z}
+ end
+
+ def full_namespace_format_regex
+ @namespace_format_regex ||= /A#{FULL_NAMESPACE_FORMAT_REGEX}\z/.freeze
+ end
+
+ def namespace_format_regex
+ @namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze
+ end
+
+ def namespace_format_message
+ "can contain only letters, digits, '_', '-' and '.'. " \
+ "Cannot start with '-' or end in '.', '.git' or '.atom'." \
+ end
+
+ def project_path_format_regex
+ @project_path_format_regex ||= /\A#{PROJECT_PATH_FORMAT_REGEX}\z/.freeze
+ end
+
+ def project_path_format_message
+ "can contain only letters, digits, '_', '-' and '.'. " \
+ "Cannot start with '-', end in '.git' or end in '.atom'" \
+ end
+
+ def archive_formats_regex
+ # |zip|tar| tar.gz | tar.bz2 |
+ @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
+ end
+
+ def git_reference_regex
+ # Valid git ref regex, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+
+ @git_reference_regex ||= single_line_regexp %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+ end
+
+ private
+
+ def single_line_regexp(regex)
+ # Turns a multiline extended regexp into a single line one,
+ # beacuse `rake routes` breaks on multiline regexes.
+ Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
+ end
+ end
+end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index f609850f8fa..e4d2a992470 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,203 +2,6 @@ module Gitlab
module Regex
extend self
- # All routes that appear on the top level must be listed here.
- # This will make sure that groups cannot be created with these names
- # as these routes would be masked by the paths already in place.
- #
- # Example:
- # /api/api-project
- #
- # the path `api` shouldn't be allowed because it would be masked by `api/*`
- #
- TOP_LEVEL_ROUTES = %w[
- -
- .well-known
- abuse_reports
- admin
- all
- api
- assets
- autocomplete
- ci
- dashboard
- explore
- files
- groups
- health_check
- help
- hooks
- import
- invites
- issues
- jwt
- koding
- member
- merge_requests
- new
- notes
- notification_settings
- oauth
- profile
- projects
- public
- repository
- robots.txt
- s
- search
- sent_notifications
- services
- snippets
- teams
- u
- unicorn_test
- unsubscribes
- uploads
- users
- ].freeze
-
- # This list should contain all words following `/*namespace_id/:project_id` in
- # routes that contain a second wildcard.
- #
- # Example:
- # /*namespace_id/:project_id/badges/*ref/build
- #
- # If `badges` was allowed as a project/group name, we would not be able to access the
- # `badges` route for those projects:
- #
- # Consider a namespace with path `foo/bar` and a project called `badges`.
- # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
- #
- # When accessing this path the route would be matched to the `badges` path
- # with the following params:
- # - namespace_id: `foo`
- # - project_id: `bar`
- # - ref: `badges/master`
- #
- # Failing to find the project, this would result in a 404.
- #
- # By rejecting `badges` the router can _count_ on the fact that `badges` will
- # be preceded by the `namespace/project`.
- PROJECT_WILDCARD_ROUTES = %w[
- badges
- blame
- blob
- builds
- commits
- create
- create_dir
- edit
- environments/folders
- files
- find_file
- gitlab-lfs/objects
- info/lfs/objects
- new
- preview
- raw
- refs
- tree
- update
- wikis
- ].freeze
-
- # These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
- # We need to reject these because we have a `/groups/*id` page that is the same
- # as the `/*id`.
- #
- # If we would allow a subgroup to be created with the name `activity` then
- # this group would not be accessible through `/groups/parent/activity` since
- # this would map to the activity-page of its parent.
- GROUP_ROUTES = %w[
- activity
- analytics
- audit_events
- avatar
- edit
- group_members
- hooks
- issues
- labels
- ldap
- ldap_group_links
- merge_requests
- milestones
- notification_setting
- pipeline_quota
- projects
- subgroups
- ].freeze
-
- ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
- ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze
-
- # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
- # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
- # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
- # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_JS` serves as a Javascript-compatible version of
- # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
- # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
- PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
- NAMESPACE_REGEX_STR_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
- NO_SUFFIX_REGEX_STR = '(?<!\.git|\.atom)'.freeze
- NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_JS})#{NO_SUFFIX_REGEX_STR}".freeze
- PROJECT_REGEX_STR = "(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX_STR}".freeze
-
- # Same as NAMESPACE_REGEX_STR but allows `/` in the path.
- # So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR
- FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze
-
- def root_namespace_route_regex
- @root_namespace_route_regex ||= begin
- illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE)
-
- single_line_regexp %r{
- (?!(#{illegal_words})/)
- #{NAMESPACE_REGEX_STR}
- }x
- end
- end
-
- def root_namespace_path_regex
- @root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z}
- end
-
- def full_namespace_path_regex
- @full_namespace_path_regex ||= %r{\A#{namespace_route_regex}/\z}
- end
-
- def full_project_path_regex
- @full_project_path_regex ||= %r{\A#{namespace_route_regex}/#{project_route_regex}/\z}
- end
-
- def namespace_regex
- @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
- end
-
- def full_namespace_regex
- @full_namespace_regex ||= %r{\A#{FULL_NAMESPACE_REGEX_STR}\z}
- end
-
- def namespace_route_regex
- @namespace_route_regex ||= begin
- illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE)
-
- single_line_regexp %r{
- #{root_namespace_route_regex}
- (?:
- /
- (?!#{illegal_words}/)
- #{NAMESPACE_REGEX_STR}
- )*
- }x
- end
- end
-
- def namespace_regex_message
- "can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-' or end in '.', '.git' or '.atom'." \
- end
-
def namespace_name_regex
@namespace_name_regex ||= /\A[\p{Alnum}\p{Pd}_\. ]*\z/.freeze
end
@@ -216,34 +19,6 @@ module Gitlab
"It must start with letter, digit, emoji or '_'."
end
- def project_path_regex
- @project_path_regex ||= %r{\A#{project_route_regex}/\z}
- end
-
- def project_route_regex
- @project_route_regex ||= begin
- illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE)
-
- single_line_regexp %r{
- (?!(#{illegal_words})/)
- #{PROJECT_REGEX_STR}
- }x
- end
- end
-
- def project_git_route_regex
- @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
- end
-
- def project_path_format_regex
- @project_path_format_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze
- end
-
- def project_path_regex_message
- "can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-', end in '.git' or end in '.atom'" \
- end
-
def file_name_regex
@file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze
end
@@ -252,36 +27,8 @@ module Gitlab
"can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
- def archive_formats_regex
- # |zip|tar| tar.gz | tar.bz2 |
- @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
- end
-
- def git_reference_regex
- # Valid git ref regex, see:
- # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
-
- @git_reference_regex ||= single_line_regexp %r{
- (?!
- (?# doesn't begins with)
- \/| (?# rule #6)
- (?# doesn't contain)
- .*(?:
- [\/.]\.| (?# rule #1,3)
- \/\/| (?# rule #6)
- @\{| (?# rule #8)
- \\ (?# rule #9)
- )
- )
- [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
- (?# doesn't end with)
- (?<!\.lock) (?# rule #1)
- (?<![\/.]) (?# rule #6-7)
- }x
- end
-
def container_registry_reference_regex
- git_reference_regex
+ Gitlab::PathRegex.git_reference_regex
end
##
@@ -315,13 +62,5 @@ module Gitlab
"can contain only lowercase letters, digits, and '-'. " \
"Must start with a letter, and cannot end with '-'"
end
-
- private
-
- def single_line_regexp(regex)
- # Turns a multiline extended regexp into a single line one,
- # beacuse `rake routes` breaks on multiline regexes.
- Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
- end
end
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 95735f43802..ad1818ff1fa 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -11,6 +11,11 @@ namespace :tokens do
reset_all_users_token(:reset_incoming_email_token!)
end
+ desc "Reset all GitLab RSS tokens"
+ task reset_all_rss: :environment do
+ reset_all_users_token(:reset_rss_token!)
+ end
+
def reset_all_users_token(reset_token_method)
TmpUser.find_in_batches do |batch|
puts "Processing batch starting with user ID: #{batch.first.id}"
@@ -35,4 +40,9 @@ class TmpUser < ActiveRecord::Base
write_new_token(:incoming_email_token)
save!(validate: false)
end
+
+ def reset_rss_token!
+ write_new_token(:rss_token)
+ save!(validate: false)
+ end
end
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/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index d40aae04fc3..3f99e2ff596 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -99,6 +99,42 @@ describe ApplicationController do
end
end
+ describe '#authenticate_user_from_rss_token' do
+ describe "authenticating a user from an RSS token" do
+ controller(described_class) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ context "when the 'rss_token' param is populated with the RSS token" do
+ context 'when the request format is atom' do
+ it "logs the user in" do
+ get :index, rss_token: user.rss_token, format: :atom
+ expect(response).to have_http_status 200
+ expect(response.body).to eq 'authenticated'
+ end
+ end
+
+ context 'when the request format is not atom' do
+ it "doesn't log the user in" do
+ get :index, rss_token: user.rss_token
+ expect(response.status).not_to have_http_status 200
+ expect(response.body).not_to eq 'authenticated'
+ end
+ end
+ end
+
+ context "when the 'rss_token' param is populated with an invalid RSS token" do
+ it "doesn't log the user" do
+ get :index, rss_token: "token"
+ expect(response.status).not_to eq 200
+ expect(response.body).not_to eq 'authenticated'
+ end
+ end
+ end
+ end
+
describe '#route_not_found' do
it 'renders 404 if authenticated' do
allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 010e3180ea4..0be7bc6a045 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -133,9 +133,13 @@ describe Import::BitbucketController do
end
context "when a namespace with the Bitbucket user's username already exists" do
- let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+ let!(:existing_namespace) { create(:group, name: other_username) }
context "when the namespace is owned by the GitLab user" do
+ before do
+ existing_namespace.add_owner(user)
+ end
+
it "takes the existing namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
@@ -146,11 +150,6 @@ describe Import::BitbucketController do
end
context "when the namespace is not owned by the GitLab user" do
- before do
- existing_namespace.owner = create(:user)
- existing_namespace.save
- end
-
it "doesn't create a project" do
expect(Gitlab::BitbucketImport::ProjectCreator).
not_to receive(:new)
@@ -202,10 +201,14 @@ describe Import::BitbucketController do
end
context 'user has chosen an existing nested namespace and name for the project' do
- let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
- let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
+ before do
+ nested_namespace.add_owner(user)
+ end
+
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params).
@@ -248,7 +251,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 3270ea059fa..3afd09063d7 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -108,9 +108,13 @@ describe Import::GitlabController do
end
context "when a namespace with the GitLab.com user's username already exists" do
- let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+ let!(:existing_namespace) { create(:group, name: other_username) }
context "when the namespace is owned by the GitLab server user" do
+ before do
+ existing_namespace.add_owner(user)
+ end
+
it "takes the existing namespace" do
expect(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, existing_namespace, user, access_params).
@@ -121,11 +125,6 @@ describe Import::GitlabController do
end
context "when the namespace is not owned by the GitLab server user" do
- before do
- existing_namespace.owner = create(:user)
- existing_namespace.save
- end
-
it "doesn't create a project" do
expect(Gitlab::GitlabImport::ProjectCreator).
not_to receive(:new)
@@ -176,8 +175,12 @@ describe Import::GitlabController do
end
context 'user has chosen an existing nested namespace for the project' do
- let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
- let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
+
+ before do
+ nested_namespace.add_owner(user)
+ end
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
@@ -221,7 +224,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 28ddd0da753..3fad4d2d658 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -20,7 +20,6 @@ FactoryGirl.define do
project factory: :empty_project
active true
properties({
- namespace: 'somepath',
api_url: 'https://kubernetes.example.com',
token: 'a' * 40
})
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 33fa80772ff..e60fe713bc3 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -8,6 +8,10 @@ FactoryGirl.define do
confirmation_token { nil }
can_create_group true
+ before(:create) do |user|
+ user.ensure_rss_token
+ end
+
trait :admin do
admin true
end
diff --git a/spec/factories/web_hook_log.rb b/spec/factories/web_hook_log.rb
new file mode 100644
index 00000000000..230b3f6b26e
--- /dev/null
+++ b/spec/factories/web_hook_log.rb
@@ -0,0 +1,14 @@
+FactoryGirl.define do
+ factory :web_hook_log do
+ web_hook factory: :project_hook
+ trigger 'push_hooks'
+ url { generate(:url) }
+ request_headers {}
+ request_data {}
+ response_headers {}
+ response_body ''
+ response_status '200'
+ execution_duration 2.0
+ internal_error_message nil
+ end
+end
diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb
new file mode 100644
index 00000000000..5b67f4de6ac
--- /dev/null
+++ b/spec/features/admin/admin_hook_logs_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Admin::HookLogs', feature: true do
+ let(:project) { create(:project) }
+ let(:system_hook) { create(:system_hook) }
+ let(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') }
+
+ before do
+ login_as :admin
+ end
+
+ scenario 'show list of hook logs' do
+ hook_log
+ visit edit_admin_hook_path(system_hook)
+
+ expect(page).to have_content('Recent Deliveries')
+ expect(page).to have_content(hook_log.url)
+ end
+
+ scenario 'show hook log details' do
+ hook_log
+ visit edit_admin_hook_path(system_hook)
+ click_link 'View details'
+
+ expect(page).to have_content("POST #{hook_log.url}")
+ expect(page).to have_content(hook_log.internal_error_message)
+ expect(page).to have_content('Resend Request')
+ end
+
+ scenario 'retry hook log' do
+ WebMock.stub_request(:post, system_hook.url)
+
+ hook_log
+ visit edit_admin_hook_path(system_hook)
+ click_link 'View details'
+ click_link 'Resend Request'
+
+ expect(current_path).to eq(edit_admin_hook_path(system_hook))
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index c5f24d412d7..80f7ec43c06 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -58,10 +58,19 @@ describe 'Admin::Hooks', feature: true do
end
describe 'Remove existing hook' do
- it 'remove existing hook' do
- visit admin_hooks_path
+ context 'removes existing hook' do
+ it 'from hooks list page' do
+ visit admin_hooks_path
+
+ expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ end
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ it 'from hook edit page' do
+ visit admin_hooks_path
+ click_link 'Edit'
+
+ expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ end
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 9ea325ab41b..711c8a710f3 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -20,13 +20,20 @@ describe "Dashboard Issues Feed", feature: true do
expect(body).to have_selector('title', text: "#{user.name} issues")
end
+ it "renders atom feed via RSS token" do
+ visit issues_dashboard_path(:atom, rss_token: user.rss_token)
+
+ expect(response_headers['Content-Type']).to have_content('application/atom+xml')
+ expect(body).to have_selector('title', text: "#{user.name} issues")
+ end
+
it "renders atom feed with url parameters" do
- visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
+ visit issues_dashboard_path(:atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
@@ -35,7 +42,7 @@ describe "Dashboard Issues Feed", feature: true do
let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
it "renders issue fields" do
- visit issues_dashboard_path(:atom, private_token: user.private_token)
+ visit issues_dashboard_path(:atom, rss_token: user.rss_token)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
@@ -58,7 +65,7 @@ describe "Dashboard Issues Feed", feature: true do
end
it "renders issue label and milestone info" do
- visit issues_dashboard_path(:atom, private_token: user.private_token)
+ visit issues_dashboard_path(:atom, rss_token: user.rss_token)
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 746df36bb25..1df058b023c 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -11,6 +11,13 @@ describe "Dashboard Feed", feature: true do
end
end
+ context "projects atom feed via RSS token" do
+ it "renders projects atom feed" do
+ visit dashboard_projects_path(:atom, rss_token: user.rss_token)
+ expect(body).to have_selector('feed title')
+ end
+ end
+
context 'feed content' do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project, author: user, description: '') }
@@ -20,7 +27,7 @@ describe "Dashboard Feed", feature: true do
project.team << [user, :master]
issue_event(issue, user)
note_event(note, user)
- visit dashboard_projects_path(:atom, private_token: user.private_token)
+ visit dashboard_projects_path(:atom, rss_token: user.rss_token)
end
it "has issue opened event" do
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 4f6754ad541..a61231ea254 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -43,25 +43,40 @@ describe 'Issues Feed', feature: true do
end
end
+ context 'when authenticated via RSS token' do
+ it 'renders atom feed' do
+ visit namespace_project_issues_path(project.namespace, project, :atom,
+ rss_token: user.rss_token)
+
+ expect(response_headers['Content-Type']).
+ to have_content('application/atom+xml')
+ expect(body).to have_selector('title', text: "#{project.name} issues")
+ expect(body).to have_selector('author email', text: issue.author_public_email)
+ expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
+ expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
+ expect(body).to have_selector('entry summary', text: issue.title)
+ end
+ end
+
it "renders atom feed with url parameters for project issues" do
visit namespace_project_issues_path(project.namespace, project,
- :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
+ :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
it "renders atom feed with url parameters for group issues" do
- visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
+ visit issues_group_path(group, :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI.parse(URI.parse(link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 7a2987e815d..fae5aaa52bd 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -11,6 +11,13 @@ describe "User Feed", feature: true do
end
end
+ context 'user atom feed via RSS token' do
+ it "renders user atom feed" do
+ visit user_path(user, :atom, rss_token: user.rss_token)
+ expect(body).to have_selector('feed title')
+ end
+ end
+
context 'feed content' do
let(:project) { create(:project) }
let(:issue) do
@@ -40,7 +47,7 @@ describe "User Feed", feature: true do
issue_event(issue, user)
note_event(note, user)
merge_request_event(merge_request, user)
- visit user_path(user, :atom, private_token: user.private_token)
+ visit user_path(user, :atom, rss_token: user.rss_token)
end
it 'has issue opened event' do
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index c977f266296..0764044260e 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe 'Dashboard Activity', feature: true do
login_as(create :user)
visit activity_dashboard_path
end
-
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 7a132dba1e9..2346a9ec2ed 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -62,6 +62,6 @@ RSpec.describe 'Dashboard Issues', feature: true do
expect(page).to have_content(other_issue.title)
end
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index f1789fc9d43..01351548a99 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -31,5 +31,5 @@ RSpec.describe 'Dashboard Projects', feature: true do
end
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index ad60fb2c74f..1c53f6dff06 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -53,10 +53,10 @@ describe "Dashboard Issues filtering", feature: true, js: true do
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index 3b481cba424..81f9c103e95 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -11,8 +11,8 @@ feature 'Group activity page', feature: true do
visit path
end
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -20,7 +20,7 @@ feature 'Group activity page', feature: true do
visit path
end
- it_behaves_like "it has an RSS button without a private token"
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index aa2e9632d6c..d6b88542ef7 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -12,15 +12,15 @@ feature 'Group issues page', feature: true do
context 'when signed in' do
let(:user) { user_in_group }
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
let(:user) { nil }
- it_behaves_like "it has an RSS button without a private token"
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index fb39693e8ca..d3c49c37374 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -11,7 +11,7 @@ feature 'Group show page', feature: true do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -19,6 +19,6 @@ feature 'Group show page', feature: true do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 03ff1cffb3f..7958ad7e24f 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -810,10 +810,10 @@ describe 'Filter issues', js: true, feature: true do
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('milestone_title' => [milestone.title])
expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
@@ -825,10 +825,10 @@ describe 'Filter issues', js: true, feature: true do
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
- expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('rss_token' => [user.rss_token])
expect(params).to include('milestone_title' => [milestone.title])
expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
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/profile_spec.rb b/spec/features/profile_spec.rb
index e63feb14b7e..7df628fd7a0 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -47,6 +47,21 @@ describe 'Profile account page', feature: true do
end
end
+ describe 'when I reset RSS token' do
+ before do
+ visit profile_account_path
+ end
+
+ it 'resets RSS token' do
+ previous_token = find("#rss-token").value
+
+ click_link('Reset RSS token')
+
+ expect(page).to have_content 'RSS token was successfully reset'
+ expect(find('#rss-token').value).not_to eq(previous_token)
+ end
+ end
+
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb
index b47c6d431eb..3c1de5c09b2 100644
--- a/spec/features/projects/activity/rss_spec.rb
+++ b/spec/features/projects/activity/rss_spec.rb
@@ -16,7 +16,7 @@ feature 'Project Activity RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
end
context 'when signed out' do
@@ -24,6 +24,6 @@ feature 'Project Activity RSS' do
visit path
end
- it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
end
end
diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb
index 6e0e1916f87..03b6d560c96 100644
--- a/spec/features/projects/commit/rss_spec.rb
+++ b/spec/features/projects/commit/rss_spec.rb
@@ -12,8 +12,8 @@ feature 'Project Commits RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -21,7 +21,7 @@ feature 'Project Commits RSS' do
visit path
end
- it_behaves_like "it has an RSS button without a private token"
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 294a63a5c6d..4162f2579d1 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -52,8 +52,12 @@ describe "Compare", js: true do
def select_using_dropdown(dropdown_type, selection)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
+ # find input before using to wait for the inputs visiblity
+ dropdown.find('.dropdown-menu')
dropdown.fill_in("Filter by Git revision", with: selection)
wait_for_requests
- dropdown.find_all("a[data-ref=\"#{selection}\"]", visible: true).last.click
+ # find before all to wait for the items visiblity
+ dropdown.find("a[data-ref=\"#{selection}\"]", match: :first)
+ dropdown.all("a[data-ref=\"#{selection}\"]").last.click
end
end
diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb
index 71429f00095..f6852192aef 100644
--- a/spec/features/projects/issues/rss_spec.rb
+++ b/spec/features/projects/issues/rss_spec.rb
@@ -16,8 +16,8 @@ feature 'Project Issues RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's private token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -25,7 +25,7 @@ feature 'Project Issues RSS' do
visit path
end
- it_behaves_like "it has an RSS button without a private token"
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb
index b1a3af612a1..53966229a2a 100644
--- a/spec/features/projects/main/rss_spec.rb
+++ b/spec/features/projects/main/rss_spec.rb
@@ -12,7 +12,7 @@ feature 'Project RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -20,6 +20,6 @@ feature 'Project RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index d3232f0cc16..fbaea14a2be 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -85,11 +85,55 @@ feature 'Integration settings', feature: true do
expect(current_path).to eq(integrations_path)
end
- scenario 'remove existing webhook' do
- hook
- visit integrations_path
+ context 'remove existing webhook' do
+ scenario 'from webhooks list page' do
+ hook
+ visit integrations_path
+
+ expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ end
+
+ scenario 'from webhook edit page' do
+ hook
+ visit integrations_path
+ click_link 'Edit'
+
+ expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ end
+ end
+ end
+
+ context 'Webhook logs' do
+ let(:hook) { create(:project_hook, project: project) }
+ let(:hook_log) { create(:web_hook_log, web_hook: hook, internal_error_message: 'some error') }
+
+ scenario 'show list of hook logs' do
+ hook_log
+ visit edit_namespace_project_hook_path(project.namespace, project, hook)
+
+ expect(page).to have_content('Recent Deliveries')
+ expect(page).to have_content(hook_log.url)
+ end
+
+ scenario 'show hook log details' do
+ hook_log
+ visit edit_namespace_project_hook_path(project.namespace, project, hook)
+ click_link 'View details'
+
+ expect(page).to have_content("POST #{hook_log.url}")
+ expect(page).to have_content(hook_log.internal_error_message)
+ expect(page).to have_content('Resend Request')
+ end
+
+ scenario 'retry hook log' do
+ WebMock.stub_request(:post, hook.url)
+
+ hook_log
+ visit edit_namespace_project_hook_path(project.namespace, project, hook)
+ click_link 'View details'
+ click_link 'Resend Request'
- expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ expect(current_path).to eq(edit_namespace_project_hook_path(project.namespace, project, hook))
end
end
end
diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb
new file mode 100644
index 00000000000..cf21b208f65
--- /dev/null
+++ b/spec/features/projects/sub_group_issuables_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'Subgroup Issuables', :feature, :js do
+ let!(:group) { create(:group, name: 'group') }
+ let!(:subgroup) { create(:group, parent: group, name: 'subgroup') }
+ let!(:project) { create(:empty_project, namespace: subgroup, name: 'project') }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ login_as user
+ end
+
+ it 'shows the full subgroup title when issues index page is empty' do
+ visit namespace_project_issues_path(project.namespace.to_param, project.to_param)
+
+ expect_to_have_full_subgroup_title
+ end
+
+ it 'shows the full subgroup title when merge requests index page is empty' do
+ visit namespace_project_merge_requests_path(project.namespace.to_param, project.to_param)
+
+ expect_to_have_full_subgroup_title
+ end
+
+ def expect_to_have_full_subgroup_title
+ title = find('.title-container')
+
+ expect(title).not_to have_selector '.initializing'
+ expect(title).to have_content 'group / subgroup / project'
+ end
+end
diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb
index 9ac51997d65..9bf59c4139c 100644
--- a/spec/features/projects/tree/rss_spec.rb
+++ b/spec/features/projects/tree/rss_spec.rb
@@ -12,7 +12,7 @@ feature 'Project Tree RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
end
context 'when signed out' do
@@ -20,6 +20,6 @@ feature 'Project Tree RSS' do
visit path
end
- it_behaves_like "an autodiscoverable RSS feed without a private token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index 14564abb16d..dbd5f66b55e 100644
--- a/spec/features/users/rss_spec.rb
+++ b/spec/features/users/rss_spec.rb
@@ -9,7 +9,7 @@ feature 'User RSS' do
visit path
end
- it_behaves_like "it has an RSS button with current_user's private token"
+ it_behaves_like "it has an RSS button with current_user's RSS token"
end
context 'when signed out' do
@@ -17,6 +17,6 @@ feature 'User RSS' do
visit path
end
- it_behaves_like "it has an RSS button without a private token"
+ it_behaves_like "it has an RSS button without an RSS token"
end
end
diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb
index f3f174f3d14..269e1057e8d 100644
--- a/spec/helpers/rss_helper_spec.rb
+++ b/spec/helpers/rss_helper_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe RssHelper do
describe '#rss_url_options' do
context 'when signed in' do
- it "includes the current_user's private_token" do
+ it "includes the current_user's rss_token" do
current_user = create(:user)
allow(helper).to receive(:current_user).and_return(current_user)
- expect(helper.rss_url_options).to include private_token: current_user.private_token
+ expect(helper.rss_url_options).to include rss_token: current_user.rss_token
end
end
context 'when signed out' do
- it "does not have a private_token" do
+ it "does not have an rss_token" do
allow(helper).to receive(:current_user).and_return(nil)
- expect(helper.rss_url_options[:private_token]).to be_nil
+ expect(helper.rss_url_options[:rss_token]).to be_nil
end
end
end
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/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
index b31a7c28ebe..c82658b9262 100644
--- a/spec/javascripts/raven/raven_config_spec.js
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -140,24 +140,6 @@ describe('RavenConfig', () => {
});
});
- describe('bindRavenErrors', () => {
- let $document;
- let $;
-
- beforeEach(() => {
- $document = jasmine.createSpyObj('$document', ['on']);
- $ = jasmine.createSpy('$').and.returnValue($document);
-
- window.$ = $;
-
- RavenConfig.bindRavenErrors();
- });
-
- it('should call .on', function () {
- expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors);
- });
- });
-
describe('handleRavenErrors', () => {
let event;
let req;
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/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/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index bd5ac6142be..3fdafd867da 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -66,16 +66,23 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
context 'using PostgreSQL' do
before do
- allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(model).to receive(:supports_drop_index_concurrently?).and_return(true)
allow(model).to receive(:disable_statement_timeout)
end
- it 'removes the index concurrently' do
+ it 'removes the index concurrently by column name' do
expect(model).to receive(:remove_index).
with(:users, { algorithm: :concurrently, column: :foo })
model.remove_concurrent_index(:users, :foo)
end
+
+ it 'removes the index concurrently by index name' do
+ expect(model).to receive(:remove_index).
+ with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
end
context 'using MySQL' do
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index c56fded7516..ce2b5d620fd 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -18,8 +18,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
let(:subject) { described_class.new(['parent/the-Path'], migration) }
it 'includes the namespace' do
- parent = create(:namespace, path: 'parent')
- child = create(:namespace, path: 'the-path', parent: parent)
+ parent = create(:group, path: 'parent')
+ child = create(:group, path: 'the-path', parent: parent)
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
@@ -30,13 +30,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
context 'for child namespaces' do
it 'only returns child namespaces with the correct path' do
- _root_namespace = create(:namespace, path: 'THE-path')
- _other_path = create(:namespace,
+ _root_namespace = create(:group, path: 'THE-path')
+ _other_path = create(:group,
path: 'other',
- parent: create(:namespace))
- namespace = create(:namespace,
+ parent: create(:group))
+ namespace = create(:group,
path: 'the-path',
- parent: create(:namespace))
+ parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
@@ -45,13 +45,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
it 'has no namespaces that look the same' do
- _root_namespace = create(:namespace, path: 'THE-path')
- _similar_path = create(:namespace,
+ _root_namespace = create(:group, path: 'THE-path')
+ _similar_path = create(:group,
path: 'not-really-the-path',
- parent: create(:namespace))
- namespace = create(:namespace,
+ parent: create(:group))
+ namespace = create(:group,
path: 'the-path',
- parent: create(:namespace))
+ parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :child).
map(&:id)
@@ -62,11 +62,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
context 'for top levelnamespaces' do
it 'only returns child namespaces with the correct path' do
- root_namespace = create(:namespace, path: 'the-path')
- _other_path = create(:namespace, path: 'other')
- _child_namespace = create(:namespace,
+ root_namespace = create(:group, path: 'the-path')
+ _other_path = create(:group, path: 'other')
+ _child_namespace = create(:group,
path: 'the-path',
- parent: create(:namespace))
+ parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id)
@@ -75,11 +75,11 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
it 'has no namespaces that just look the same' do
- root_namespace = create(:namespace, path: 'the-path')
- _similar_path = create(:namespace, path: 'not-really-the-path')
- _child_namespace = create(:namespace,
+ root_namespace = create(:group, path: 'the-path')
+ _similar_path = create(:group, path: 'not-really-the-path')
+ _child_namespace = create(:group,
path: 'the-path',
- parent: create(:namespace))
+ parent: create(:group))
found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id)
@@ -124,10 +124,10 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
describe "#child_ids_for_parent" do
it "collects child ids for all levels" do
- parent = create(:namespace)
- first_child = create(:namespace, parent: parent)
- second_child = create(:namespace, parent: parent)
- third_child = create(:namespace, parent: second_child)
+ parent = create(:group)
+ first_child = create(:group, parent: parent)
+ second_child = create(:group, parent: parent)
+ third_child = create(:group, parent: second_child)
all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id])
@@ -205,9 +205,9 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
end
describe '#rename_namespaces' do
- let!(:top_level_namespace) { create(:namespace, path: 'the-path') }
+ let!(:top_level_namespace) { create(:group, path: 'the-path') }
let!(:child_namespace) do
- create(:namespace, path: 'the-path', parent: create(:namespace))
+ create(:group, path: 'the-path', parent: create(:group))
end
it 'renames top level namespaces the namespace' do
diff --git a/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
new file mode 100644
index 00000000000..df77f4037af
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/cartfile_linker_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::CartfileLinker, lib: true do
+ describe '.support?' do
+ it 'supports Cartfile' do
+ expect(described_class.support?('Cartfile')).to be_truthy
+ end
+
+ it 'supports Cartfile.private' do
+ expect(described_class.support?('Cartfile.private')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('test.Cartfile')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "Cartfile" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ # Require version 2.3.1 or later
+ github "ReactiveCocoa/ReactiveCocoa" >= 2.3.1
+
+ # Require version 1.x
+ github "Mantle/Mantle" ~> 1.0 # (1.0 or later, but less than 2.0)
+
+ # Require exactly version 0.4.1
+ github "jspahrsummers/libextobjc" == 0.4.1
+
+ # Use the latest version
+ github "jspahrsummers/xcconfigs"
+
+ # Use the branch
+ github "jspahrsummers/xcconfigs" "branch"
+
+ # Use a project from GitHub Enterprise
+ github "https://enterprise.local/ghe/desktop/git-error-translations"
+
+ # Use a project from any arbitrary server, on the "development" branch
+ git "https://enterprise.local/desktop/git-error-translations2.git" "development"
+
+ # Use a local project
+ git "file:///directory/to/project" "branch"
+
+ # A binary only framework
+ binary "https://my.domain.com/release/MyFramework.json" ~> 2.3
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('ReactiveCocoa/ReactiveCocoa', 'https://github.com/ReactiveCocoa/ReactiveCocoa'))
+ expect(subject).to include(link('Mantle/Mantle', 'https://github.com/Mantle/Mantle'))
+ expect(subject).to include(link('jspahrsummers/libextobjc', 'https://github.com/jspahrsummers/libextobjc'))
+ expect(subject).to include(link('jspahrsummers/xcconfigs', 'https://github.com/jspahrsummers/xcconfigs'))
+ end
+
+ it 'links Git repos' do
+ expect(subject).to include(link('https://enterprise.local/ghe/desktop/git-error-translations', 'https://enterprise.local/ghe/desktop/git-error-translations'))
+ expect(subject).to include(link('https://enterprise.local/desktop/git-error-translations2.git', 'https://enterprise.local/desktop/git-error-translations2.git'))
+ end
+
+ it 'links binary-only frameworks' do
+ expect(subject).to include(link('https://my.domain.com/release/MyFramework.json', 'https://my.domain.com/release/MyFramework.json'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
new file mode 100644
index 00000000000..d7a926e800f
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -0,0 +1,82 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::ComposerJsonLinker, lib: true do
+ describe '.support?' do
+ it 'supports composer.json' do
+ expect(described_class.support?('composer.json')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('composer.json.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "composer.json" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ {
+ "name": "laravel/laravel",
+ "homepage": "https://laravel.com/",
+ "description": "The Laravel Framework.",
+ "keywords": ["framework", "laravel"],
+ "license": "MIT",
+ "type": "project",
+ "repositories": [
+ {
+ "type": "git",
+ "url": "https://github.com/laravel/laravel.git"
+ }
+ ],
+ "require": {
+ "php": ">=5.5.9",
+ "laravel/framework": "5.2.*"
+ },
+ "require-dev": {
+ "fzaninotto/faker": "~1.4",
+ "mockery/mockery": "0.9.*",
+ "phpunit/phpunit": "~4.0",
+ "symfony/css-selector": "2.8.*|3.0.*",
+ "symfony/dom-crawler": "2.8.*|3.0.*"
+ }
+ }
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the module name' do
+ expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
+ end
+
+ it 'links the homepage' do
+ expect(subject).to include(link('https://laravel.com/', 'https://laravel.com/'))
+ end
+
+ it 'links the repository URL' do
+ expect(subject).to include(link('https://github.com/laravel/laravel.git', 'https://github.com/laravel/laravel.git'))
+ end
+
+ it 'links the license' do
+ expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('laravel/framework', 'https://packagist.org/packages/laravel/framework'))
+ expect(subject).to include(link('fzaninotto/faker', 'https://packagist.org/packages/fzaninotto/faker'))
+ expect(subject).to include(link('mockery/mockery', 'https://packagist.org/packages/mockery/mockery'))
+ expect(subject).to include(link('phpunit/phpunit', 'https://packagist.org/packages/phpunit/phpunit'))
+ expect(subject).to include(link('symfony/css-selector', 'https://packagist.org/packages/symfony/css-selector'))
+ expect(subject).to include(link('symfony/dom-crawler', 'https://packagist.org/packages/symfony/dom-crawler'))
+ end
+
+ it 'does not link core dependencies' do
+ expect(subject).not_to include(link('php', 'https://packagist.org/packages/php'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
index 2e52097a946..3f8335f03ea 100644
--- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::DependencyLinker::GemfileLinker, lib: true do
subject { Gitlab::Highlight.highlight(file_name, file_content) }
def link(name, url)
- %{<a href="#{url}" rel="noopener noreferrer" target="_blank">#{name}</a>}
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
it 'links sources' do
diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
new file mode 100644
index 00000000000..d4a71403939
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::GemspecLinker, lib: true do
+ describe '.support?' do
+ it 'supports *.gemspec' do
+ expect(described_class.support?('gitlab_git.gemspec')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('.gemspec.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "gitlab_git.gemspec" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ Gem::Specification.new do |s|
+ s.name = 'gitlab_git'
+ s.version = `cat VERSION`
+ s.date = Time.now.strftime('%Y-%m-%d')
+ s.summary = "Gitlab::Git library"
+ s.description = "GitLab wrapper around git objects"
+ s.authors = ["Dmitriy Zaporozhets"]
+ s.email = 'dmitriy.zaporozhets@gmail.com'
+ s.license = 'MIT'
+ s.files = `git ls-files lib/`.split('\n') << 'VERSION'
+ s.homepage = 'https://gitlab.com/gitlab-org/gitlab_git'
+
+ s.add_dependency('github-linguist', '~> 4.7.0')
+ s.add_dependency('activesupport', '~> 4.0')
+ s.add_dependency('rugged', '~> 0.24.0')
+ s.add_runtime_dependency('charlock_holmes', '~> 0.7.3')
+ s.add_development_dependency('listen', '~> 3.0.6')
+ end
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the gem name' do
+ expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
+ end
+
+ it 'links the license' do
+ expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/'))
+ end
+
+ it 'links the homepage' do
+ expect(subject).to include(link('https://gitlab.com/gitlab-org/gitlab_git', 'https://gitlab.com/gitlab-org/gitlab_git'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('github-linguist', 'https://rubygems.org/gems/github-linguist'))
+ expect(subject).to include(link('activesupport', 'https://rubygems.org/gems/activesupport'))
+ expect(subject).to include(link('rugged', 'https://rubygems.org/gems/rugged'))
+ expect(subject).to include(link('charlock_holmes', 'https://rubygems.org/gems/charlock_holmes'))
+ expect(subject).to include(link('listen', 'https://rubygems.org/gems/listen'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
new file mode 100644
index 00000000000..e279e0c9019
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/godeps_json_linker_spec.rb
@@ -0,0 +1,84 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::GodepsJsonLinker, lib: true do
+ describe '.support?' do
+ it 'supports Godeps.json' do
+ expect(described_class.support?('Godeps.json')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('Godeps.json.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "Godeps.json" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ {
+ "ImportPath": "gitlab.com/gitlab-org/gitlab-pages",
+ "GoVersion": "go1.5",
+ "Packages": [
+ "./..."
+ ],
+ "Deps": [
+ {
+ "ImportPath": "github.com/kardianos/osext",
+ "Rev": "efacde03154693404c65e7aa7d461ac9014acd0c"
+ },
+ {
+ "ImportPath": "github.com/stretchr/testify/assert",
+ "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b"
+ },
+ {
+ "ImportPath": "github.com/stretchr/testify/require",
+ "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b"
+ },
+ {
+ "ImportPath": "gitlab.com/group/project/path",
+ "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b"
+ },
+ {
+ "ImportPath": "gitlab.com/group/subgroup/project.git/path",
+ "Rev": "1297dc01ed0a819ff634c89707081a4df43baf6b"
+ },
+ {
+ "ImportPath": "golang.org/x/crypto/ssh/terminal",
+ "Rev": "1351f936d976c60a0a48d728281922cf63eafb8d"
+ },
+ {
+ "ImportPath": "golang.org/x/net/http2",
+ "Rev": "b4e17d61b15679caf2335da776c614169a1b4643"
+ }
+ ]
+ }
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the package name' do
+ expect(subject).to include(link('gitlab.com/gitlab-org/gitlab-pages', 'https://gitlab.com/gitlab-org/gitlab-pages'))
+ end
+
+ it 'links GitHub repos' do
+ expect(subject).to include(link('github.com/kardianos/osext', 'https://github.com/kardianos/osext'))
+ expect(subject).to include(link('github.com/stretchr/testify/assert', 'https://github.com/stretchr/testify/tree/master/assert'))
+ expect(subject).to include(link('github.com/stretchr/testify/require', 'https://github.com/stretchr/testify/tree/master/require'))
+ end
+
+ it 'links GitLab projects' do
+ expect(subject).to include(link('gitlab.com/group/project/path', 'https://gitlab.com/group/project/tree/master/path'))
+ expect(subject).to include(link('gitlab.com/group/subgroup/project.git/path', 'https://gitlab.com/group/subgroup/project/tree/master/path'))
+ end
+
+ it 'links Golang packages' do
+ expect(subject).to include(link('golang.org/x/net/http2', 'https://godoc.org/golang.org/x/net/http2'))
+ end
+ end
+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
new file mode 100644
index 00000000000..8c979ae1869
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -0,0 +1,94 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::PackageJsonLinker, lib: true do
+ describe '.support?' do
+ it 'supports package.json' do
+ expect(described_class.support?('package.json')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('package.json.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "package.json" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1",
+ "repository": {
+ "type": "git",
+ "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",
+ "karma": "^1.4.1"
+ },
+ "devDependencies": {
+ "vows": "^0.7.0",
+ "assume": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0",
+ "pre-commit": "*"
+ },
+ "license": "MIT"
+ }
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the module name' do
+ expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name'))
+ end
+
+ it 'links the homepage' do
+ expect(subject).to include(link('https://github.com/vuejs/vue#readme', 'https://github.com/vuejs/vue#readme'))
+ end
+
+ it 'links the repository URL' do
+ expect(subject).to include(link('https://github.com/vuejs/vue.git', 'https://github.com/vuejs/vue.git'))
+ end
+
+ it 'links the license' do
+ expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('primus', 'https://npmjs.com/package/primus'))
+ expect(subject).to include(link('async', 'https://npmjs.com/package/async'))
+ 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'))
+ end
+
+ it 'links GitHub repos' do
+ expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet'))
+ end
+
+ 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/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
new file mode 100644
index 00000000000..06007cf97f7
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::PodfileLinker, lib: true do
+ describe '.support?' do
+ it 'supports Podfile' do
+ expect(described_class.support?('Podfile')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('Podfile.lock')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "Podfile" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ source 'https://github.com/artsy/Specs.git'
+ source 'https://github.com/CocoaPods/Specs.git'
+
+ platform :ios, '8.0'
+ use_frameworks!
+ inhibit_all_warnings!
+
+ target 'Artsy' do
+ pod 'AFNetworking', "~> 2.5"
+ pod 'Interstellar/Core', git: 'https://github.com/ashfurrow/Interstellar.git', branch: 'observable-unsubscribe'
+ end
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links sources' do
+ expect(subject).to include(link('https://github.com/artsy/Specs.git', 'https://github.com/artsy/Specs.git'))
+ expect(subject).to include(link('https://github.com/CocoaPods/Specs.git', 'https://github.com/CocoaPods/Specs.git'))
+ end
+
+ it 'links packages' do
+ expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
+ expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar'))
+ end
+
+ it 'links Git repos' do
+ expect(subject).to include(link('https://github.com/ashfurrow/Interstellar.git', 'https://github.com/ashfurrow/Interstellar.git'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
new file mode 100644
index 00000000000..d722865264b
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/podspec_json_linker_spec.rb
@@ -0,0 +1,96 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::PodspecJsonLinker, lib: true do
+ describe '.support?' do
+ it 'supports *.podspec.json' do
+ expect(described_class.support?('Reachability.podspec.json')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('.podspec.json.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "AFNetworking.podspec.json" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ {
+ "name": "AFNetworking",
+ "version": "2.0.0",
+ "license": "MIT",
+ "summary": "A delightful iOS and OS X networking framework.",
+ "homepage": "https://github.com/AFNetworking/AFNetworking",
+ "authors": {
+ "Mattt Thompson": "m@mattt.me"
+ },
+ "source": {
+ "git": "https://github.com/AFNetworking/AFNetworking.git",
+ "tag": "2.0.0",
+ "submodules": true
+ },
+ "requires_arc": true,
+ "platforms": {
+ "ios": "6.0",
+ "osx": "10.8"
+ },
+ "public_header_files": "AFNetworking/*.h",
+ "subspecs": [
+ {
+ "name": "NSURLConnection",
+ "dependencies": {
+ "AFNetworking/Serialization": [
+
+ ],
+ "AFNetworking/Reachability": [
+
+ ],
+ "AFNetworking/Security": [
+
+ ]
+ },
+ "source_files": [
+ "AFNetworking/AFURLConnectionOperation.{h,m}",
+ "AFNetworking/AFHTTPRequestOperation.{h,m}",
+ "AFNetworking/AFHTTPRequestOperationManager.{h,m}"
+ ]
+ }
+ ]
+ }
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the gem name' do
+ expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
+ end
+
+ it 'links the license' do
+ expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/'))
+ end
+
+ it 'links the homepage' do
+ expect(subject).to include(link('https://github.com/AFNetworking/AFNetworking', 'https://github.com/AFNetworking/AFNetworking'))
+ end
+
+ it 'links the source URL' do
+ expect(subject).to include(link('https://github.com/AFNetworking/AFNetworking.git', 'https://github.com/AFNetworking/AFNetworking.git'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('AFNetworking/Serialization', 'https://cocoapods.org/pods/AFNetworking'))
+ expect(subject).to include(link('AFNetworking/Reachability', 'https://cocoapods.org/pods/AFNetworking'))
+ expect(subject).to include(link('AFNetworking/Security', 'https://cocoapods.org/pods/AFNetworking'))
+ end
+
+ it 'does not link subspec names' do
+ expect(subject).not_to include(link('NSURLConnection', 'https://cocoapods.org/pods/NSURLConnection'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
new file mode 100644
index 00000000000..dfc366b5817
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
@@ -0,0 +1,69 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::PodspecLinker, lib: true do
+ describe '.support?' do
+ it 'supports *.podspec' do
+ expect(described_class.support?('Reachability.podspec')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('.podspec.example')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "Reachability.podspec" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ Pod::Spec.new do |spec|
+ spec.name = 'Reachability'
+ spec.version = '3.1.0'
+ spec.license = { :type => 'GPL-3.0' }
+ spec.license = "MIT"
+ spec.license = { type: 'Apache-2.0' }
+ spec.homepage = 'https://github.com/tonymillion/Reachability'
+ spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' }
+ spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.'
+ spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' }
+ spec.source_files = 'Reachability.{h,m}'
+ spec.framework = 'SystemConfiguration'
+
+ spec.dependency 'AFNetworking', '~> 1.0'
+ spec.dependency 'RestKit/CoreData', '~> 0.20.0'
+ spec.ios.dependency 'MBProgressHUD', '~> 0.5'
+ end
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links the gem name' do
+ expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
+ end
+
+ it 'links the license' do
+ expect(subject).to include(link('GPL-3.0', 'http://choosealicense.com/licenses/gpl-3.0/'))
+ expect(subject).to include(link('MIT', 'http://choosealicense.com/licenses/mit/'))
+ expect(subject).to include(link('Apache-2.0', 'http://choosealicense.com/licenses/apache-2.0/'))
+ end
+
+ it 'links the homepage' do
+ expect(subject).to include(link('https://github.com/tonymillion/Reachability', 'https://github.com/tonymillion/Reachability'))
+ end
+
+ it 'links the source URL' do
+ expect(subject).to include(link('https://github.com/tonymillion/Reachability.git', 'https://github.com/tonymillion/Reachability.git'))
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
+ expect(subject).to include(link('RestKit/CoreData', 'https://cocoapods.org/pods/RestKit'))
+ expect(subject).to include(link('MBProgressHUD', 'https://cocoapods.org/pods/MBProgressHUD'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
new file mode 100644
index 00000000000..4da8821726c
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb
@@ -0,0 +1,87 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do
+ describe '.support?' do
+ it 'supports requirements.txt' do
+ expect(described_class.support?('requirements.txt')).to be_truthy
+ end
+
+ it 'supports doc-requirements.txt' do
+ expect(described_class.support?('doc-requirements.txt')).to be_truthy
+ end
+
+ it 'does not support other files' do
+ expect(described_class.support?('requirements')).to be_falsey
+ end
+ end
+
+ describe '#link' do
+ let(:file_name) { "requirements.txt" }
+
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ #
+ ####### example-requirements.txt #######
+ #
+ ###### Requirements without Version Specifiers ######
+ nose
+ nose-cov
+ beautifulsoup4
+ #
+ ###### Requirements with Version Specifiers ######
+ # See https://www.python.org/dev/peps/pep-0440/#version-specifiers
+ docopt == 0.6.1 # Version Matching. Must be version 0.6.1
+ keyring >= 4.1.1 # Minimum version 4.1.1
+ coverage != 3.5 # Version Exclusion. Anything except version 3.5
+ Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.*
+ #
+ ###### Refer to other requirements files ######
+ -r other-requirements.txt
+ #
+ #
+ ###### A particular file ######
+ ./downloads/numpy-1.9.2-cp34-none-win32.whl
+ http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl
+ #
+ ###### Additional Requirements without Version Specifiers ######
+ # Same as 1st section, just here to show that you can put things in any order.
+ rejected
+ green
+ #
+
+ Jinja2>=2.3
+ Pygments>=1.2
+ Sphinx>=1.3
+ docutils>=0.7
+ markupsafe
+ CONTENT
+ end
+
+ subject { Gitlab::Highlight.highlight(file_name, file_content) }
+
+ def link(name, url)
+ %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
+ end
+
+ it 'links dependencies' do
+ expect(subject).to include(link('nose', 'https://pypi.python.org/pypi/nose'))
+ expect(subject).to include(link('nose-cov', 'https://pypi.python.org/pypi/nose-cov'))
+ expect(subject).to include(link('beautifulsoup4', 'https://pypi.python.org/pypi/beautifulsoup4'))
+ expect(subject).to include(link('docopt', 'https://pypi.python.org/pypi/docopt'))
+ expect(subject).to include(link('keyring', 'https://pypi.python.org/pypi/keyring'))
+ expect(subject).to include(link('coverage', 'https://pypi.python.org/pypi/coverage'))
+ expect(subject).to include(link('Mopidy-Dirble', 'https://pypi.python.org/pypi/Mopidy-Dirble'))
+ expect(subject).to include(link('rejected', 'https://pypi.python.org/pypi/rejected'))
+ expect(subject).to include(link('green', 'https://pypi.python.org/pypi/green'))
+ expect(subject).to include(link('Jinja2', 'https://pypi.python.org/pypi/Jinja2'))
+ expect(subject).to include(link('Pygments', 'https://pypi.python.org/pypi/Pygments'))
+ expect(subject).to include(link('Sphinx', 'https://pypi.python.org/pypi/Sphinx'))
+ expect(subject).to include(link('docutils', 'https://pypi.python.org/pypi/docutils'))
+ expect(subject).to include(link('markupsafe', 'https://pypi.python.org/pypi/markupsafe'))
+ end
+
+ it 'links URLs' do
+ expect(subject).to include(link('http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl', 'http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl'))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker_spec.rb b/spec/lib/gitlab/dependency_linker_spec.rb
index 03d5b61d70c..3d1cfbcfbf7 100644
--- a/spec/lib/gitlab/dependency_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker_spec.rb
@@ -9,5 +9,77 @@ describe Gitlab::DependencyLinker, lib: true do
described_class.link(blob_name, nil, nil)
end
+
+ it 'links using GemspecLinker' do
+ blob_name = 'gitlab_git.gemspec'
+
+ expect(described_class::GemspecLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using PackageJsonLinker' do
+ blob_name = 'package.json'
+
+ expect(described_class::PackageJsonLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using ComposerJsonLinker' do
+ blob_name = 'composer.json'
+
+ expect(described_class::ComposerJsonLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using PodfileLinker' do
+ blob_name = 'Podfile'
+
+ expect(described_class::PodfileLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using PodspecLinker' do
+ blob_name = 'Reachability.podspec'
+
+ expect(described_class::PodspecLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using PodspecJsonLinker' do
+ blob_name = 'AFNetworking.podspec.json'
+
+ expect(described_class::PodspecJsonLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using CartfileLinker' do
+ blob_name = 'Cartfile'
+
+ expect(described_class::CartfileLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using GodepsJsonLinker' do
+ blob_name = 'Godeps.json'
+
+ expect(described_class::GodepsJsonLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
+
+ it 'links using RequirementsTxtLinker' do
+ blob_name = 'requirements.txt'
+
+ expect(described_class::RequirementsTxtLinker).to receive(:link)
+
+ described_class.link(blob_name, nil, nil)
+ end
end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index cdf0af6d7ef..7095104d75c 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Diff::Position, lib: true do
it "returns the correct diff file" do
diff_file = subject.diff_file(project.repository)
- expect(diff_file.new_file).to be true
+ expect(diff_file.new_file?).to be true
expect(diff_file.new_path).to eq(subject.new_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs)
end
@@ -314,7 +314,7 @@ describe Gitlab::Diff::Position, lib: true do
it "returns the correct diff file" do
diff_file = subject.diff_file(project.repository)
- expect(diff_file.deleted_file).to be true
+ expect(diff_file.deleted_file?).to be true
expect(diff_file.old_path).to eq(subject.old_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs)
end
@@ -356,7 +356,7 @@ describe Gitlab::Diff::Position, lib: true do
it "returns the correct diff file" do
diff_file = subject.diff_file(project.repository)
- expect(diff_file.new_file).to be true
+ expect(diff_file.new_file?).to be true
expect(diff_file.new_path).to eq(subject.new_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs)
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/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/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e57b3053871..a20cef3b000 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -59,8 +59,6 @@ describe Gitlab::Highlight, lib: true do
end
describe '#highlight' do
- subject { described_class.highlight(file_name, file_content, nowrap: false) }
-
it 'links dependencies via DependencyLinker' do
expect(Gitlab::DependencyLinker).to receive(:link).
with('file.name', 'Contents', anything).and_call_original
diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb
index 52f2614d5ca..a3dbeaa3753 100644
--- a/spec/lib/gitlab/i18n_spec.rb
+++ b/spec/lib/gitlab/i18n_spec.rb
@@ -1,27 +1,27 @@
require 'spec_helper'
-module Gitlab
- describe I18n, lib: true do
- let(:user) { create(:user, preferred_language: 'es') }
+describe Gitlab::I18n, lib: true do
+ let(:user) { create(:user, preferred_language: 'es') }
- describe '.set_locale' do
- it 'sets the locale based on current user preferred language' do
- Gitlab::I18n.set_locale(user)
+ describe '.locale=' do
+ after { described_class.use_default_locale }
- expect(FastGettext.locale).to eq('es')
- expect(::I18n.locale).to eq(:es)
- end
+ it 'sets the locale based on current user preferred language' do
+ described_class.locale = user.preferred_language
+
+ expect(FastGettext.locale).to eq('es')
+ expect(::I18n.locale).to eq(:es)
end
+ end
- describe '.reset_locale' do
- it 'resets the locale to the default language' do
- Gitlab::I18n.set_locale(user)
+ describe '.use_default_locale' do
+ it 'resets the locale to the default language' do
+ described_class.locale = user.preferred_language
- Gitlab::I18n.reset_locale
+ described_class.use_default_locale
- expect(FastGettext.locale).to eq('en')
- expect(::I18n.locale).to eq(:en)
- end
+ expect(FastGettext.locale).to eq('en')
+ expect(::I18n.locale).to eq(:en)
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 34f617e23a5..2e9646286df 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -131,6 +131,7 @@ services:
- service_hook
hooks:
- project
+- web_hook_logs
protected_branches:
- project
- merge_access_levels
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
new file mode 100644
index 00000000000..1eea710c80b
--- /dev/null
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -0,0 +1,384 @@
+# coding: utf-8
+require 'spec_helper'
+
+describe Gitlab::PathRegex, lib: true do
+ # Pass in a full path to remove the format segment:
+ # `/ci/lint(.:format)` -> `/ci/lint`
+ def without_format(path)
+ path.split('(', 2)[0]
+ end
+
+ # Pass in a full path and get the last segment before a wildcard
+ # That's not a parameter
+ # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
+ # -> 'builds/artifacts'
+ def path_before_wildcard(path)
+ path = path.gsub(STARTING_WITH_NAMESPACE, "")
+ path_segments = path.split('/').reject(&:empty?)
+ wildcard_index = path_segments.index { |segment| parameter?(segment) }
+
+ segments_before_wildcard = path_segments[0..wildcard_index - 1]
+
+ segments_before_wildcard.join('/')
+ end
+
+ def parameter?(segment)
+ segment =~ /[*:]/
+ end
+
+ # If the path is reserved. Then no conflicting paths can# be created for any
+ # route using this reserved word.
+ #
+ # Both `builds/artifacts` & `build` are covered by reserving the word
+ # `build`
+ def wildcards_include?(path)
+ described_class::PROJECT_WILDCARD_ROUTES.include?(path) ||
+ described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first)
+ end
+
+ def failure_message(missing_words, constant_name, migration_helper)
+ missing_words = Array(missing_words)
+ <<-MSG
+ Found new routes that could cause conflicts with existing namespaced routes
+ for groups or projects.
+
+ Add <#{missing_words.join(', ')}> to `Gitlab::PathRegex::#{constant_name}
+ to make sure no projects or namespaces can be created with those paths.
+
+ To rename any existing records with those paths you can use the
+ `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
+ migration helper.
+
+ Make sure to make a note of the renamed records in the release blog post.
+
+ MSG
+ end
+
+ let(:all_routes) do
+ route_set = Rails.application.routes
+ routes_collection = route_set.routes
+ routes_array = routes_collection.routes
+ routes_array.map { |route| route.path.spec.to_s }
+ end
+
+ let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
+
+ # Routes not starting with `/:` or `/*`
+ # all routes not starting with a param
+ let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
+
+ let(:top_level_words) do
+ routes_not_starting_in_wildcard.map do |route|
+ route.split('/')[1]
+ end.compact.uniq
+ end
+
+ # All routes that start with a namespaced path, that have 1 or more
+ # path-segments before having another wildcard parameter.
+ # - Starting with paths:
+ # - `/*namespace_id/:project_id/`
+ # - `/*namespace_id/:id/`
+ # - Followed by one or more path-parts not starting with `:` or `*`
+ # - Followed by a path-part that includes a wildcard parameter `*`
+ # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
+ STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
+ NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
+ ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
+ WILDCARD_SEGMENT = %r{\*}
+ let(:namespaced_wildcard_routes) do
+ routes_without_format.select do |p|
+ p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
+ end
+ end
+
+ # This will return all paths that are used in a namespaced route
+ # before another wildcard path:
+ #
+ # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
+ # /*namespace_id/:project_id/info/lfs/objects/*oid
+ # /*namespace_id/:project_id/commits/*id
+ # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
+ # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
+ let(:all_wildcard_paths) do
+ namespaced_wildcard_routes.map do |route|
+ path_before_wildcard(route)
+ end.uniq
+ end
+
+ STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
+ let(:group_routes) do
+ routes_without_format.select do |path|
+ path =~ STARTING_WITH_GROUP
+ end
+ end
+
+ let(:paths_after_group_id) do
+ group_routes.map do |route|
+ route.gsub(STARTING_WITH_GROUP, '').split('/').first
+ end.uniq
+ end
+
+ describe 'TOP_LEVEL_ROUTES' do
+ it 'includes all the top level namespaces' do
+ failure_block = lambda do
+ missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
+ failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
+ end
+
+ expect(described_class::TOP_LEVEL_ROUTES)
+ .to include(*top_level_words), failure_block
+ end
+ end
+
+ describe 'GROUP_ROUTES' do
+ it "don't contain a second wildcard" do
+ failure_block = lambda do
+ missing_words = paths_after_group_id - described_class::GROUP_ROUTES
+ failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
+ end
+
+ expect(described_class::GROUP_ROUTES)
+ .to include(*paths_after_group_id), failure_block
+ end
+ end
+
+ describe 'PROJECT_WILDCARD_ROUTES' do
+ it 'includes all paths that can be used after a namespace/project path' do
+ aggregate_failures do
+ all_wildcard_paths.each do |path|
+ expect(wildcards_include?(path))
+ .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths')
+ end
+ end
+ end
+ end
+
+ describe '.root_namespace_path_regex' do
+ subject { described_class.root_namespace_path_regex }
+
+ it 'rejects top level routes' do
+ expect(subject).not_to match('admin/')
+ expect(subject).not_to match('api/')
+ expect(subject).not_to match('.well-known/')
+ end
+
+ it 'accepts project wildcard routes' do
+ expect(subject).to match('blob/')
+ expect(subject).to match('edit/')
+ expect(subject).to match('wikis/')
+ end
+
+ it 'accepts group routes' do
+ expect(subject).to match('activity/')
+ expect(subject).to match('group_members/')
+ expect(subject).to match('subgroups/')
+ end
+
+ it 'is not case sensitive' do
+ expect(subject).not_to match('Users/')
+ end
+
+ it 'does not allow extra slashes' do
+ expect(subject).not_to match('/blob/')
+ expect(subject).not_to match('blob//')
+ end
+ end
+
+ describe '.full_namespace_path_regex' do
+ subject { described_class.full_namespace_path_regex }
+
+ context 'at the top level' do
+ context 'when the final level' do
+ it 'rejects top level routes' do
+ expect(subject).not_to match('admin/')
+ expect(subject).not_to match('api/')
+ expect(subject).not_to match('.well-known/')
+ end
+
+ it 'accepts project wildcard routes' do
+ expect(subject).to match('blob/')
+ expect(subject).to match('edit/')
+ expect(subject).to match('wikis/')
+ end
+
+ it 'accepts group routes' do
+ expect(subject).to match('activity/')
+ expect(subject).to match('group_members/')
+ expect(subject).to match('subgroups/')
+ end
+ end
+
+ context 'when more levels follow' do
+ it 'rejects top level routes' do
+ expect(subject).not_to match('admin/more/')
+ expect(subject).not_to match('api/more/')
+ expect(subject).not_to match('.well-known/more/')
+ end
+
+ it 'accepts project wildcard routes' do
+ expect(subject).to match('blob/more/')
+ expect(subject).to match('edit/more/')
+ expect(subject).to match('wikis/more/')
+ expect(subject).to match('environments/folders/')
+ expect(subject).to match('info/lfs/objects/')
+ end
+
+ it 'accepts group routes' do
+ expect(subject).to match('activity/more/')
+ expect(subject).to match('group_members/more/')
+ expect(subject).to match('subgroups/more/')
+ end
+ end
+ end
+
+ context 'at the second level' do
+ context 'when the final level' do
+ it 'accepts top level routes' do
+ expect(subject).to match('root/admin/')
+ expect(subject).to match('root/api/')
+ expect(subject).to match('root/.well-known/')
+ end
+
+ it 'rejects project wildcard routes' do
+ expect(subject).not_to match('root/blob/')
+ expect(subject).not_to match('root/edit/')
+ expect(subject).not_to match('root/wikis/')
+ expect(subject).not_to match('root/environments/folders/')
+ expect(subject).not_to match('root/info/lfs/objects/')
+ end
+
+ it 'rejects group routes' do
+ expect(subject).not_to match('root/activity/')
+ expect(subject).not_to match('root/group_members/')
+ expect(subject).not_to match('root/subgroups/')
+ end
+ end
+
+ context 'when more levels follow' do
+ it 'accepts top level routes' do
+ expect(subject).to match('root/admin/more/')
+ expect(subject).to match('root/api/more/')
+ expect(subject).to match('root/.well-known/more/')
+ end
+
+ it 'rejects project wildcard routes' do
+ expect(subject).not_to match('root/blob/more/')
+ expect(subject).not_to match('root/edit/more/')
+ expect(subject).not_to match('root/wikis/more/')
+ expect(subject).not_to match('root/environments/folders/more/')
+ expect(subject).not_to match('root/info/lfs/objects/more/')
+ end
+
+ it 'rejects group routes' do
+ expect(subject).not_to match('root/activity/more/')
+ expect(subject).not_to match('root/group_members/more/')
+ expect(subject).not_to match('root/subgroups/more/')
+ end
+ end
+ end
+
+ it 'is not case sensitive' do
+ expect(subject).not_to match('root/Blob/')
+ end
+
+ it 'does not allow extra slashes' do
+ expect(subject).not_to match('/root/admin/')
+ expect(subject).not_to match('root/admin//')
+ end
+ end
+
+ describe '.project_path_regex' do
+ subject { described_class.project_path_regex }
+
+ it 'accepts top level routes' do
+ expect(subject).to match('admin/')
+ expect(subject).to match('api/')
+ expect(subject).to match('.well-known/')
+ end
+
+ it 'rejects project wildcard routes' do
+ expect(subject).not_to match('blob/')
+ expect(subject).not_to match('edit/')
+ expect(subject).not_to match('wikis/')
+ expect(subject).not_to match('environments/folders/')
+ expect(subject).not_to match('info/lfs/objects/')
+ end
+
+ it 'accepts group routes' do
+ expect(subject).to match('activity/')
+ expect(subject).to match('group_members/')
+ expect(subject).to match('subgroups/')
+ end
+
+ it 'is not case sensitive' do
+ expect(subject).not_to match('Blob/')
+ end
+
+ it 'does not allow extra slashes' do
+ expect(subject).not_to match('/admin/')
+ expect(subject).not_to match('admin//')
+ end
+ end
+
+ describe '.full_project_path_regex' do
+ subject { described_class.full_project_path_regex }
+
+ it 'accepts top level routes' do
+ expect(subject).to match('root/admin/')
+ expect(subject).to match('root/api/')
+ expect(subject).to match('root/.well-known/')
+ end
+
+ it 'rejects project wildcard routes' do
+ expect(subject).not_to match('root/blob/')
+ expect(subject).not_to match('root/edit/')
+ expect(subject).not_to match('root/wikis/')
+ expect(subject).not_to match('root/environments/folders/')
+ expect(subject).not_to match('root/info/lfs/objects/')
+ end
+
+ it 'accepts group routes' do
+ expect(subject).to match('root/activity/')
+ expect(subject).to match('root/group_members/')
+ expect(subject).to match('root/subgroups/')
+ end
+
+ it 'is not case sensitive' do
+ expect(subject).not_to match('root/Blob/')
+ end
+
+ it 'does not allow extra slashes' do
+ expect(subject).not_to match('/root/admin/')
+ expect(subject).not_to match('root/admin//')
+ end
+ end
+
+ describe '.namespace_format_regex' do
+ subject { described_class.namespace_format_regex }
+
+ it { is_expected.to match('gitlab-ce') }
+ it { is_expected.to match('gitlab_git') }
+ it { is_expected.to match('_underscore.js') }
+ it { is_expected.to match('100px.com') }
+ it { is_expected.to match('gitlab.org') }
+ it { is_expected.not_to match('?gitlab') }
+ it { is_expected.not_to match('git lab') }
+ it { is_expected.not_to match('gitlab.git') }
+ it { is_expected.not_to match('gitlab.org.') }
+ it { is_expected.not_to match('gitlab.org/') }
+ it { is_expected.not_to match('/gitlab.org') }
+ it { is_expected.not_to match('gitlab git') }
+ end
+
+ describe '.project_path_format_regex' do
+ subject { described_class.project_path_format_regex }
+
+ it { is_expected.to match('gitlab-ce') }
+ it { is_expected.to match('gitlab_git') }
+ it { is_expected.to match('_underscore.js') }
+ it { is_expected.to match('100px.com') }
+ it { is_expected.not_to match('?gitlab') }
+ it { is_expected.not_to match('git lab') }
+ it { is_expected.not_to match('gitlab.git') }
+ end
+end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index a7d1283acb8..0bee892fe0c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -2,386 +2,6 @@
require 'spec_helper'
describe Gitlab::Regex, lib: true do
- # Pass in a full path to remove the format segment:
- # `/ci/lint(.:format)` -> `/ci/lint`
- def without_format(path)
- path.split('(', 2)[0]
- end
-
- # Pass in a full path and get the last segment before a wildcard
- # That's not a parameter
- # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
- # -> 'builds/artifacts'
- def path_before_wildcard(path)
- path = path.gsub(STARTING_WITH_NAMESPACE, "")
- path_segments = path.split('/').reject(&:empty?)
- wildcard_index = path_segments.index { |segment| parameter?(segment) }
-
- segments_before_wildcard = path_segments[0..wildcard_index - 1]
-
- segments_before_wildcard.join('/')
- end
-
- def parameter?(segment)
- segment =~ /[*:]/
- end
-
- # If the path is reserved. Then no conflicting paths can# be created for any
- # route using this reserved word.
- #
- # Both `builds/artifacts` & `build` are covered by reserving the word
- # `build`
- def wildcards_include?(path)
- described_class::PROJECT_WILDCARD_ROUTES.include?(path) ||
- described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first)
- end
-
- def failure_message(missing_words, constant_name, migration_helper)
- missing_words = Array(missing_words)
- <<-MSG
- Found new routes that could cause conflicts with existing namespaced routes
- for groups or projects.
-
- Add <#{missing_words.join(', ')}> to `Gitlab::Regex::#{constant_name}
- to make sure no projects or namespaces can be created with those paths.
-
- To rename any existing records with those paths you can use the
- `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
- migration helper.
-
- Make sure to make a note of the renamed records in the release blog post.
-
- MSG
- end
-
- let(:all_routes) do
- route_set = Rails.application.routes
- routes_collection = route_set.routes
- routes_array = routes_collection.routes
- routes_array.map { |route| route.path.spec.to_s }
- end
-
- let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
-
- # Routes not starting with `/:` or `/*`
- # all routes not starting with a param
- let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
-
- let(:top_level_words) do
- routes_not_starting_in_wildcard.map do |route|
- route.split('/')[1]
- end.compact.uniq
- end
-
- # All routes that start with a namespaced path, that have 1 or more
- # path-segments before having another wildcard parameter.
- # - Starting with paths:
- # - `/*namespace_id/:project_id/`
- # - `/*namespace_id/:id/`
- # - Followed by one or more path-parts not starting with `:` or `*`
- # - Followed by a path-part that includes a wildcard parameter `*`
- # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
- STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
- NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
- ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
- WILDCARD_SEGMENT = %r{\*}
- let(:namespaced_wildcard_routes) do
- routes_without_format.select do |p|
- p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
- end
- end
-
- # This will return all paths that are used in a namespaced route
- # before another wildcard path:
- #
- # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
- # /*namespace_id/:project_id/info/lfs/objects/*oid
- # /*namespace_id/:project_id/commits/*id
- # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
- # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
- let(:all_wildcard_paths) do
- namespaced_wildcard_routes.map do |route|
- path_before_wildcard(route)
- end.uniq
- end
-
- STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
- let(:group_routes) do
- routes_without_format.select do |path|
- path =~ STARTING_WITH_GROUP
- end
- end
-
- let(:paths_after_group_id) do
- group_routes.map do |route|
- route.gsub(STARTING_WITH_GROUP, '').split('/').first
- end.uniq
- end
-
- describe 'TOP_LEVEL_ROUTES' do
- it 'includes all the top level namespaces' do
- failure_block = lambda do
- missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
- failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
- end
-
- expect(described_class::TOP_LEVEL_ROUTES)
- .to include(*top_level_words), failure_block
- end
- end
-
- describe 'GROUP_ROUTES' do
- it "don't contain a second wildcard" do
- failure_block = lambda do
- missing_words = paths_after_group_id - described_class::GROUP_ROUTES
- failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
- end
-
- expect(described_class::GROUP_ROUTES)
- .to include(*paths_after_group_id), failure_block
- end
- end
-
- describe 'PROJECT_WILDCARD_ROUTES' do
- it 'includes all paths that can be used after a namespace/project path' do
- aggregate_failures do
- all_wildcard_paths.each do |path|
- expect(wildcards_include?(path))
- .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths')
- end
- end
- end
- end
-
- describe '.root_namespace_path_regex' do
- subject { described_class.root_namespace_path_regex }
-
- it 'rejects top level routes' do
- expect(subject).not_to match('admin/')
- expect(subject).not_to match('api/')
- expect(subject).not_to match('.well-known/')
- end
-
- it 'accepts project wildcard routes' do
- expect(subject).to match('blob/')
- expect(subject).to match('edit/')
- expect(subject).to match('wikis/')
- end
-
- it 'accepts group routes' do
- expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
- end
-
- it 'is not case sensitive' do
- expect(subject).not_to match('Users/')
- end
-
- it 'does not allow extra slashes' do
- expect(subject).not_to match('/blob/')
- expect(subject).not_to match('blob//')
- end
- end
-
- describe '.full_namespace_path_regex' do
- subject { described_class.full_namespace_path_regex }
-
- context 'at the top level' do
- context 'when the final level' do
- it 'rejects top level routes' do
- expect(subject).not_to match('admin/')
- expect(subject).not_to match('api/')
- expect(subject).not_to match('.well-known/')
- end
-
- it 'accepts project wildcard routes' do
- expect(subject).to match('blob/')
- expect(subject).to match('edit/')
- expect(subject).to match('wikis/')
- end
-
- it 'accepts group routes' do
- expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
- end
- end
-
- context 'when more levels follow' do
- it 'rejects top level routes' do
- expect(subject).not_to match('admin/more/')
- expect(subject).not_to match('api/more/')
- expect(subject).not_to match('.well-known/more/')
- end
-
- it 'accepts project wildcard routes' do
- expect(subject).to match('blob/more/')
- expect(subject).to match('edit/more/')
- expect(subject).to match('wikis/more/')
- expect(subject).to match('environments/folders/')
- expect(subject).to match('info/lfs/objects/')
- end
-
- it 'accepts group routes' do
- expect(subject).to match('activity/more/')
- expect(subject).to match('group_members/more/')
- expect(subject).to match('subgroups/more/')
- end
- end
- end
-
- context 'at the second level' do
- context 'when the final level' do
- it 'accepts top level routes' do
- expect(subject).to match('root/admin/')
- expect(subject).to match('root/api/')
- expect(subject).to match('root/.well-known/')
- end
-
- it 'rejects project wildcard routes' do
- expect(subject).not_to match('root/blob/')
- expect(subject).not_to match('root/edit/')
- expect(subject).not_to match('root/wikis/')
- expect(subject).not_to match('root/environments/folders/')
- expect(subject).not_to match('root/info/lfs/objects/')
- end
-
- it 'rejects group routes' do
- expect(subject).not_to match('root/activity/')
- expect(subject).not_to match('root/group_members/')
- expect(subject).not_to match('root/subgroups/')
- end
- end
-
- context 'when more levels follow' do
- it 'accepts top level routes' do
- expect(subject).to match('root/admin/more/')
- expect(subject).to match('root/api/more/')
- expect(subject).to match('root/.well-known/more/')
- end
-
- it 'rejects project wildcard routes' do
- expect(subject).not_to match('root/blob/more/')
- expect(subject).not_to match('root/edit/more/')
- expect(subject).not_to match('root/wikis/more/')
- expect(subject).not_to match('root/environments/folders/more/')
- expect(subject).not_to match('root/info/lfs/objects/more/')
- end
-
- it 'rejects group routes' do
- expect(subject).not_to match('root/activity/more/')
- expect(subject).not_to match('root/group_members/more/')
- expect(subject).not_to match('root/subgroups/more/')
- end
- end
- end
-
- it 'is not case sensitive' do
- expect(subject).not_to match('root/Blob/')
- end
-
- it 'does not allow extra slashes' do
- expect(subject).not_to match('/root/admin/')
- expect(subject).not_to match('root/admin//')
- end
- end
-
- describe '.project_path_regex' do
- subject { described_class.project_path_regex }
-
- it 'accepts top level routes' do
- expect(subject).to match('admin/')
- expect(subject).to match('api/')
- expect(subject).to match('.well-known/')
- end
-
- it 'rejects project wildcard routes' do
- expect(subject).not_to match('blob/')
- expect(subject).not_to match('edit/')
- expect(subject).not_to match('wikis/')
- expect(subject).not_to match('environments/folders/')
- expect(subject).not_to match('info/lfs/objects/')
- end
-
- it 'accepts group routes' do
- expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
- end
-
- it 'is not case sensitive' do
- expect(subject).not_to match('Blob/')
- end
-
- it 'does not allow extra slashes' do
- expect(subject).not_to match('/admin/')
- expect(subject).not_to match('admin//')
- end
- end
-
- describe '.full_project_path_regex' do
- subject { described_class.full_project_path_regex }
-
- it 'accepts top level routes' do
- expect(subject).to match('root/admin/')
- expect(subject).to match('root/api/')
- expect(subject).to match('root/.well-known/')
- end
-
- it 'rejects project wildcard routes' do
- expect(subject).not_to match('root/blob/')
- expect(subject).not_to match('root/edit/')
- expect(subject).not_to match('root/wikis/')
- expect(subject).not_to match('root/environments/folders/')
- expect(subject).not_to match('root/info/lfs/objects/')
- end
-
- it 'accepts group routes' do
- expect(subject).to match('root/activity/')
- expect(subject).to match('root/group_members/')
- expect(subject).to match('root/subgroups/')
- end
-
- it 'is not case sensitive' do
- expect(subject).not_to match('root/Blob/')
- end
-
- it 'does not allow extra slashes' do
- expect(subject).not_to match('/root/admin/')
- expect(subject).not_to match('root/admin//')
- end
- end
-
- describe '.namespace_regex' do
- subject { described_class.namespace_regex }
-
- it { is_expected.to match('gitlab-ce') }
- it { is_expected.to match('gitlab_git') }
- it { is_expected.to match('_underscore.js') }
- it { is_expected.to match('100px.com') }
- it { is_expected.to match('gitlab.org') }
- it { is_expected.not_to match('?gitlab') }
- it { is_expected.not_to match('git lab') }
- it { is_expected.not_to match('gitlab.git') }
- it { is_expected.not_to match('gitlab.org.') }
- it { is_expected.not_to match('gitlab.org/') }
- it { is_expected.not_to match('/gitlab.org') }
- it { is_expected.not_to match('gitlab git') }
- end
-
- describe '.project_path_format_regex' do
- subject { described_class.project_path_format_regex }
-
- it { is_expected.to match('gitlab-ce') }
- it { is_expected.to match('gitlab_git') }
- it { is_expected.to match('_underscore.js') }
- it { is_expected.to match('100px.com') }
- it { is_expected.not_to match('?gitlab') }
- it { is_expected.not_to match('git lab') }
- it { is_expected.not_to match('gitlab.git') }
- end
-
describe '.project_name_regex' do
subject { described_class.project_name_regex }
@@ -412,16 +32,4 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('9foo') }
it { is_expected.not_to match('foo-') }
end
-
- describe '.full_namespace_regex' do
- subject { described_class.full_namespace_regex }
-
- it { is_expected.to match('gitlab.org') }
- it { is_expected.to match('gitlab.org/gitlab-git') }
- it { is_expected.not_to match('gitlab.org.') }
- it { is_expected.not_to match('gitlab.org/') }
- it { is_expected.not_to match('/gitlab.org') }
- it { is_expected.not_to match('gitlab.git') }
- it { is_expected.not_to match('gitlab git') }
- end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1e6260270fe..ec6f6c42eac 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -128,6 +128,15 @@ describe Notify do
is_expected.to have_body_text(namespace_project_issue_path(project.namespace, project, issue))
end
end
+
+ context 'with a preferred language' do
+ before { Gitlab::I18n.locale = :es }
+ after { Gitlab::I18n.use_default_locale }
+
+ it 'always generates the email using the default language' do
+ is_expected.to have_body_text('foo, bar, and baz')
+ end
+ end
end
describe 'status changed' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 56b24ce62f3..c8023dc13b1 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -965,7 +965,7 @@ describe Ci::Pipeline, models: true do
end
before do
- ProjectWebHookWorker.drain
+ WebHookWorker.drain
end
context 'with pipeline hooks enabled' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index ab4c51a87b0..96f075d4f7d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -145,7 +145,7 @@ describe DiffNote, models: true do
context "when the merge request's diff refs don't match that of the diff note" do
before do
- allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs)
+ allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
end
it "returns false" do
@@ -194,7 +194,7 @@ describe DiffNote, models: true do
context "when the note is outdated" do
before do
- allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs)
+ allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
end
it "uses the DiffPositionUpdateService" do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 1a83c836652..57454d2a773 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,36 +1,19 @@
-require "spec_helper"
+require 'spec_helper'
describe ServiceHook, models: true do
- describe "Associations" do
+ describe 'associations' do
it { is_expected.to belong_to :service }
end
- describe "execute" do
- before(:each) do
- @service_hook = create(:service_hook)
- @data = { project_id: 1, data: {} }
+ describe 'execute' do
+ let(:hook) { build(:service_hook) }
+ let(:data) { { key: 'value' } }
- WebMock.stub_request(:post, @service_hook.url)
- end
-
- it "POSTs to the webhook URL" do
- @service_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @service_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
- ).once
- end
-
- it "POSTs the data as JSON" do
- @service_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @service_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
- ).once
- end
-
- it "catches exceptions" do
- expect(WebHook).to receive(:post).and_raise("Some HTTP Post error")
+ it '#execute' do
+ expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original
+ expect_any_instance_of(WebHookService).to receive(:execute)
- expect { @service_hook.execute(@data) }.to raise_error(RuntimeError)
+ hook.execute(data)
end
end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 4340170888d..0d2b622132e 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -126,4 +126,26 @@ describe SystemHook, models: true do
expect(SystemHook.repository_update_hooks).to eq([hook])
end
end
+
+ describe 'execute WebHookService' do
+ let(:hook) { build(:system_hook) }
+ let(:data) { { key: 'value' } }
+ let(:hook_name) { 'system_hook' }
+
+ before do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
+ end
+
+ it '#execute' do
+ expect_any_instance_of(WebHookService).to receive(:execute)
+
+ hook.execute(data, hook_name)
+ end
+
+ it '#async_execute' do
+ expect_any_instance_of(WebHookService).to receive(:async_execute)
+
+ hook.async_execute(data, hook_name)
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
new file mode 100644
index 00000000000..c649cf3b589
--- /dev/null
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+describe WebHookLog, models: true do
+ it { is_expected.to belong_to(:web_hook) }
+
+ it { is_expected.to serialize(:request_headers).as(Hash) }
+ it { is_expected.to serialize(:request_data).as(Hash) }
+ it { is_expected.to serialize(:response_headers).as(Hash) }
+
+ it { is_expected.to validate_presence_of(:web_hook) }
+
+ describe '#success?' do
+ let(:web_hook_log) { build(:web_hook_log, response_status: status) }
+
+ describe '2xx' do
+ let(:status) { '200' }
+ it { expect(web_hook_log.success?).to be_truthy }
+ end
+
+ describe 'not 2xx' do
+ let(:status) { '500' }
+ it { expect(web_hook_log.success?).to be_falsey }
+ end
+
+ describe 'internal erorr' do
+ let(:status) { 'internal error' }
+ it { expect(web_hook_log.success?).to be_falsey }
+ end
+ end
+end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 9d4db1bfb52..53157c24477 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,89 +1,54 @@
require 'spec_helper'
describe WebHook, models: true do
- describe "Validations" do
+ let(:hook) { build(:project_hook) }
+
+ describe 'associations' do
+ it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) }
+ end
+
+ describe 'validations' do
it { is_expected.to validate_presence_of(:url) }
describe 'url' do
- it { is_expected.to allow_value("http://example.com").for(:url) }
- it { is_expected.to allow_value("https://example.com").for(:url) }
- it { is_expected.to allow_value(" https://example.com ").for(:url) }
- it { is_expected.to allow_value("http://test.com/api").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
+ it { is_expected.to allow_value('http://example.com').for(:url) }
+ it { is_expected.to allow_value('https://example.com').for(:url) }
+ it { is_expected.to allow_value(' https://example.com ').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api?key=abc').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api?key=abc&type=def').for(:url) }
- it { is_expected.not_to allow_value("example.com").for(:url) }
- it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
- it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+ it { is_expected.not_to allow_value('example.com').for(:url) }
+ it { is_expected.not_to allow_value('ftp://example.com').for(:url) }
+ it { is_expected.not_to allow_value('herp-and-derp').for(:url) }
it 'strips :url before saving it' do
- hook = create(:project_hook, url: ' https://example.com ')
+ hook.url = ' https://example.com '
+ hook.save
expect(hook.url).to eq('https://example.com')
end
end
end
- describe "execute" do
- let(:project) { create(:empty_project) }
- let(:project_hook) { create(:project_hook) }
-
- before(:each) do
- project.hooks << [project_hook]
- @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
-
- WebMock.stub_request(:post, project_hook.url)
- end
-
- context 'when token is defined' do
- let(:project_hook) { create(:project_hook, :token) }
-
- it 'POSTs to the webhook URL' do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json',
- 'X-Gitlab-Event' => 'Push Hook',
- 'X-Gitlab-Token' => project_hook.token }
- ).once
- end
- end
-
- it "POSTs to the webhook URL" do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
- ).once
- end
-
- it "POSTs the data as JSON" do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
- ).once
- end
-
- it "catches exceptions" do
- expect(WebHook).to receive(:post).and_raise("Some HTTP Post error")
-
- expect { project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
- end
-
- it "handles SSL exceptions" do
- expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
+ describe 'execute' do
+ let(:data) { { key: 'value' } }
+ let(:hook_name) { 'project hook' }
- expect(project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
+ before do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
end
- it "handles 200 status code" do
- WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success")
+ it '#execute' do
+ expect_any_instance_of(WebHookService).to receive(:execute)
- expect(project_hook.execute(@data, 'push_hooks')).to eq([200, 'Success'])
+ hook.execute(data, hook_name)
end
- it "handles 2xx status codes" do
- WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success")
+ it '#async_execute' do
+ expect_any_instance_of(WebHookService).to receive(:async_execute)
- expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success'])
+ hook.async_execute(data, hook_name)
end
end
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 80ca19acdda..84867e3d96b 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -49,6 +49,23 @@ describe Label, models: true do
expect(label.color).to eq('#abcdef')
end
+
+ it 'uses default color if color is missing' do
+ label = described_class.new(color: nil)
+
+ expect(label.color).to be(Label::DEFAULT_COLOR)
+ end
+ end
+
+ describe '#text_color' do
+ it 'uses default color if color is missing' do
+ expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR).
+ and_return(spy)
+
+ label = described_class.new(color: nil)
+
+ label.text_color
+ end
end
describe '#title' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index da915c49d3c..712470d6bf5 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1243,7 +1243,7 @@ describe MergeRequest, models: true do
end
end
- describe "#diff_sha_refs" do
+ describe "#diff_refs" do
context "with diffs" do
subject { create(:merge_request, :with_diffs) }
@@ -1252,7 +1252,7 @@ describe MergeRequest, models: true do
expect_any_instance_of(Repository).not_to receive(:commit)
- subject.diff_sha_refs
+ subject.diff_refs
end
it "returns expected diff_refs" do
@@ -1262,7 +1262,7 @@ describe MergeRequest, models: true do
head_sha: subject.merge_request_diff.head_commit_sha
)
- expect(subject.diff_sha_refs).to eq(expected_diff_refs)
+ expect(subject.diff_refs).to eq(expected_diff_refs)
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 312302afdbb..ff5e7c350aa 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -238,8 +238,8 @@ describe Namespace, models: true do
end
context 'in sub-groups' do
- let(:parent) { create(:namespace, path: 'parent') }
- let(:child) { create(:namespace, parent: parent, path: 'child') }
+ let(:parent) { create(:group, path: 'parent') }
+ let(:child) { create(:group, parent: parent, path: 'child') }
let!(:project) { create(:project_empty_repo, namespace: child) }
let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') }
let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") }
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_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index c1c2f2a7219..0dcf4a4b5d6 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -13,7 +13,7 @@ describe KubernetesService, models: true, caching: true do
let(:discovery_url) { service.api_url + '/api/v1' }
let(:discovery_response) { { body: kube_discovery_body.to_json } }
- let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" }
+ let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" }
let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }
def stub_kubeclient_discover
@@ -100,7 +100,35 @@ describe KubernetesService, models: true, caching: true do
it 'sets the namespace to the default' do
expect(kube_namespace).not_to be_nil
- expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
+ expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { service.actual_namespace }
+
+ it "returns the default namespace" do
+ is_expected.to eq(service.send(:default_namespace))
+ end
+
+ context 'when namespace is specified' do
+ before do
+ service.namespace = 'my-namespace'
+ end
+
+ it "returns the user-namespace" do
+ is_expected.to eq('my-namespace')
+ end
+ end
+
+ context 'when service is not assigned to project' do
+ before do
+ service.project = nil
+ end
+
+ it "does not return namespace" do
+ is_expected.to be_nil
end
end
end
@@ -187,13 +215,14 @@ describe KubernetesService, models: true, caching: true do
kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
expect(kube_namespace).not_to be_nil
- expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
end
end
end
describe '#terminals' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
+
subject { service.terminals(environment) }
context 'with invalid pods' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6a15830a15c..aabdac4bb75 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -440,6 +440,22 @@ describe User, models: true do
end
end
+ describe 'ensure incoming email token' do
+ it 'has incoming email token' do
+ user = create(:user)
+ expect(user.incoming_email_token).not_to be_blank
+ end
+ end
+
+ describe 'rss token' do
+ it 'ensures an rss token on read' do
+ user = create(:user, rss_token: nil)
+ rss_token = user.rss_token
+ expect(rss_token).not_to be_blank
+ expect(user.reload.rss_token).to eq rss_token
+ end
+ end
+
describe '#recently_sent_password_reset?' do
it 'is false when reset_password_sent_at is nil' do
user = build_stubbed(:user, reset_password_sent_at: nil)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 4919ad19833..a2503dbeb69 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -287,7 +287,7 @@ describe API::Users do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.namespace_regex_message])
+ to eq([Gitlab::PathRegex.namespace_format_message])
end
it "is not available for non admin users" do
@@ -459,7 +459,7 @@ describe API::Users do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.namespace_regex_message])
+ to eq([Gitlab::PathRegex.namespace_format_message])
end
it 'returns 400 if provider is missing for identity update' do
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index e5fc0b676af..179fc9733ad 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -103,6 +103,18 @@ describe Admin::HooksController, "routing" do
end
end
+# admin_hook_hook_log_retry GET /admin/hooks/:hook_id/hook_logs/:id/retry(.:format) admin/hook_logs#retry
+# admin_hook_hook_log GET /admin/hooks/:hook_id/hook_logs/:id(.:format) admin/hook_logs#show
+describe Admin::HookLogsController, 'routing' do
+ it 'to #retry' do
+ expect(get('/admin/hooks/1/hook_logs/1/retry')).to route_to('admin/hook_logs#retry', hook_id: '1', id: '1')
+ end
+
+ it 'to #show' do
+ expect(get('/admin/hooks/1/hook_logs/1')).to route_to('admin/hook_logs#show', hook_id: '1', id: '1')
+ end
+end
+
# admin_logs GET /admin/logs(.:format) admin/logs#show
describe Admin::LogsController, "routing" do
it "to #show" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a391c046f92..54417f6b3e1 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -349,6 +349,18 @@ describe 'project routing' do
end
end
+ # retry_namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id/retry(.:format) projects/hook_logs#retry
+ # namespace_project_hook_hook_log GET /:project_id/hooks/:hook_id/hook_logs/:id(.:format) projects/hook_logs#show
+ describe Projects::HookLogsController, 'routing' do
+ it 'to #retry' do
+ expect(get('/gitlab/gitlabhq/hooks/1/hook_logs/1/retry')).to route_to('projects/hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1')
+ end
+
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/hooks/1/hook_logs/1')).to route_to('projects/hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', hook_id: '1', id: '1')
+ end
+ end
+
# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/}
describe Projects::CommitController, 'routing' do
it 'to #show' do
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index abacc50a371..a62af13cf0c 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -151,6 +151,10 @@ describe ProfilesController, "routing" do
expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token')
end
+ it "to #reset_rss_token" do
+ expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
+ end
+
it "to #show" do
expect(get("/profile")).to route_to('profiles#show')
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/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
new file mode 100644
index 00000000000..b5abc46e80c
--- /dev/null
+++ b/spec/services/web_hook_service_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe WebHookService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:project_hook) { create(:project_hook) }
+ let(:headers) do
+ {
+ 'Content-Type' => 'application/json',
+ 'X-Gitlab-Event' => 'Push Hook'
+ }
+ end
+ let(:data) do
+ { before: 'oldrev', after: 'newrev', ref: 'ref' }
+ end
+ let(:service_instance) { WebHookService.new(project_hook, data, 'push_hooks') }
+
+ describe '#execute' do
+ before(:each) do
+ project.hooks << [project_hook]
+
+ WebMock.stub_request(:post, project_hook.url)
+ end
+
+ context 'when token is defined' do
+ let(:project_hook) { create(:project_hook, :token) }
+
+ it 'POSTs to the webhook URL' do
+ service_instance.execute
+ expect(WebMock).to have_requested(:post, project_hook.url).with(
+ headers: headers.merge({ 'X-Gitlab-Token' => project_hook.token })
+ ).once
+ end
+ end
+
+ it 'POSTs to the webhook URL' do
+ service_instance.execute
+ expect(WebMock).to have_requested(:post, project_hook.url).with(
+ headers: headers
+ ).once
+ end
+
+ it 'POSTs the data as JSON' do
+ service_instance.execute
+ expect(WebMock).to have_requested(:post, project_hook.url).with(
+ headers: headers
+ ).once
+ end
+
+ it 'catches exceptions' do
+ WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error'))
+
+ expect { service_instance.execute }.to raise_error(StandardError)
+ end
+
+ it 'handles exceptions' do
+ exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout]
+ exceptions.each do |exception_class|
+ exception = exception_class.new('Exception message')
+
+ WebMock.stub_request(:post, project_hook.url).to_raise(exception)
+ expect(service_instance.execute).to eq([nil, exception.message])
+ expect { service_instance.execute }.not_to raise_error
+ end
+ end
+
+ it 'handles 200 status code' do
+ WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success')
+
+ expect(service_instance.execute).to eq([200, 'Success'])
+ end
+
+ it 'handles 2xx status codes' do
+ WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: 'Success')
+
+ expect(service_instance.execute).to eq([201, 'Success'])
+ end
+
+ context 'execution logging' do
+ let(:hook_log) { project_hook.web_hook_logs.last }
+
+ context 'with success' do
+ before do
+ WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success')
+ service_instance.execute
+ end
+
+ it 'log successful execution' do
+ expect(hook_log.trigger).to eq('push_hooks')
+ expect(hook_log.url).to eq(project_hook.url)
+ expect(hook_log.request_headers).to eq(headers)
+ expect(hook_log.response_body).to eq('Success')
+ expect(hook_log.response_status).to eq('200')
+ expect(hook_log.execution_duration).to be > 0
+ expect(hook_log.internal_error_message).to be_nil
+ end
+ end
+
+ context 'with exception' do
+ before do
+ WebMock.stub_request(:post, project_hook.url).to_raise(SocketError.new('Some HTTP Post error'))
+ service_instance.execute
+ end
+
+ it 'log failed execution' do
+ expect(hook_log.trigger).to eq('push_hooks')
+ expect(hook_log.url).to eq(project_hook.url)
+ expect(hook_log.request_headers).to eq(headers)
+ expect(hook_log.response_body).to eq('')
+ expect(hook_log.response_status).to eq('internal error')
+ expect(hook_log.execution_duration).to be > 0
+ expect(hook_log.internal_error_message).to eq('Some HTTP Post error')
+ end
+ end
+
+ context 'should not log ServiceHooks' do
+ let(:service_hook) { create(:service_hook) }
+ let(:service_instance) { WebHookService.new(service_hook, data, 'service_hook') }
+
+ before do
+ WebMock.stub_request(:post, service_hook.url).to_return(status: 200, body: 'Success')
+ end
+
+ it { expect { service_instance.execute }.not_to change(WebHookLog, :count) }
+ end
+ end
+ end
+
+ describe '#async_execute' do
+ let(:system_hook) { create(:system_hook) }
+
+ it 'enqueue WebHookWorker' do
+ expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks')
+
+ WebHookService.new(project_hook, data, 'push_hooks').async_execute
+ end
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index c59b30c772d..d6b40db09ce 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -209,9 +209,13 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen a namespace and name for the project' do
- let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+ let(:test_namespace) { create(:group, name: 'test_namespace') }
let(:test_name) { 'test_name' }
+ before do
+ test_namespace.add_owner(user)
+ end
+
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider).
@@ -230,10 +234,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen an existing nested namespace and name for the project' do
- let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
- let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
+ before do
+ nested_namespace.add_owner(user)
+ end
+
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider).
@@ -276,7 +284,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index 9a3b0a731ad..1cbb4134995 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -1,23 +1,23 @@
-shared_examples "an autodiscoverable RSS feed with current_user's private token" do
- it "has an RSS autodiscovery link tag with current_user's private token" do
- expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false)
+shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do
+ it "has an RSS autodiscovery link tag with current_user's RSS token" do
+ expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{Thread.current[:current_user].rss_token}']", visible: false)
end
end
-shared_examples "it has an RSS button with current_user's private token" do
- it "shows the RSS button with current_user's private token" do
- expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']")
+shared_examples "it has an RSS button with current_user's RSS token" do
+ it "shows the RSS button with current_user's RSS token" do
+ expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{Thread.current[:current_user].rss_token}']")
end
end
-shared_examples "an autodiscoverable RSS feed without a private token" do
- it "has an RSS autodiscovery link tag without a private token" do
- expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false)
+shared_examples "an autodiscoverable RSS feed without an RSS token" do
+ it "has an RSS autodiscovery link tag without an RSS token" do
+ expect(page).to have_css("link[type*='atom+xml']:not([href*='rss_token'])", visible: false)
end
end
-shared_examples "it has an RSS button without a private token" do
- it "shows the RSS button without a private token" do
- expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])")
+shared_examples "it has an RSS button without an RSS token" do
+ it "shows the RSS button without an RSS token" do
+ expect(page).to have_css("a:has(.fa-rss):not([href*='rss_token'])")
end
end
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/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index d2a1ded57ff..9280fad4ace 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -41,7 +41,7 @@ module KubernetesHelpers
containers.map do |container|
terminal = {
selectors: { pod: pod_name, container: container['name'] },
- url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
+ url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index b168098edea..3c000feba5d 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -123,7 +123,7 @@ module TestEnv
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
- unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
+ unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
raise "Can't clone gitaly"
end
@@ -252,4 +252,15 @@ module TestEnv
cleanup && init unless reset.call
end
end
+
+ def gitaly_needs_update?(gitaly_dir)
+ gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip
+
+ # Notice that this will always yield true when using branch versions
+ # (`=branch_name`), but that actually makes sure the server is always based
+ # on the latest branch revision.
+ gitaly_version != Gitlab::GitalyClient.expected_server_version
+ rescue Errno::ENOENT
+ true
+ end
end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 19036c7677c..b84137eb365 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -18,4 +18,10 @@ describe 'tokens rake tasks' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
end
end
+
+ describe 'reset_all_rss task' do
+ it 'invokes create_hooks task' do
+ expect { run_rake_task('tokens:reset_all_rss') }.to change { user.reload.rss_token }
+ end
+ end
end
diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb
index 03e23781d1b..5f998e78f07 100644
--- a/spec/validators/dynamic_path_validator_spec.rb
+++ b/spec/validators/dynamic_path_validator_spec.rb
@@ -15,31 +15,31 @@ describe DynamicPathValidator do
end
context 'for group' do
- it 'calls valid_namespace_path?' do
+ it 'calls valid_group_path?' do
group = build(:group, :nested, path: 'activity')
- expect(described_class).to receive(:valid_namespace_path?).with(group.full_path).and_call_original
+ expect(described_class).to receive(:valid_group_path?).with(group.full_path).and_call_original
expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey
end
end
context 'for user' do
- it 'calls valid_namespace_path?' do
+ it 'calls valid_user_path?' do
user = build(:user, username: 'activity')
- expect(described_class).to receive(:valid_namespace_path?).with(user.full_path).and_call_original
+ expect(described_class).to receive(:valid_user_path?).with(user.full_path).and_call_original
expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy
end
end
context 'for user namespace' do
- it 'calls valid_namespace_path?' do
+ it 'calls valid_user_path?' do
user = create(:user, username: 'activity')
namespace = user.namespace
- expect(described_class).to receive(:valid_namespace_path?).with(namespace.full_path).and_call_original
+ expect(described_class).to receive(:valid_user_path?).with(namespace.full_path).and_call_original
expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy
end
@@ -52,7 +52,7 @@ describe DynamicPathValidator do
validator.validate_each(group, :path, "Path with spaces, and comma's!")
- expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message)
+ expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
end
it 'adds a message when the path is not in the correct format' do
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 6295856b461..4e036285e8c 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -20,14 +20,6 @@ describe ProcessCommitWorker do
worker.perform(project.id, -1, commit.to_hash)
end
- it 'does not process the commit when no issues are referenced' do
- allow(worker).to receive(:build_commit).and_return(double(matches_cross_reference_regex?: false))
-
- expect(worker).not_to receive(:process_commit_message)
-
- worker.perform(project.id, user.id, commit.to_hash)
- end
-
it 'processes the commit message' do
expect(worker).to receive(:process_commit_message).and_call_original
@@ -39,6 +31,18 @@ describe ProcessCommitWorker do
worker.perform(project.id, user.id, commit.to_hash)
end
+
+ context 'when commit already exists in upstream project' do
+ let(:forked) { create(:project, :public) }
+
+ it 'does not process commit message' do
+ create(:forked_project_link, forked_to_project: forked, forked_from_project: project)
+
+ expect(worker).not_to receive(:process_commit_message)
+
+ worker.perform(forked.id, user.id, forked.commit.to_hash)
+ end
+ end
end
describe '#process_commit_message' do
diff --git a/spec/workers/remove_old_web_hook_logs_worker_spec.rb b/spec/workers/remove_old_web_hook_logs_worker_spec.rb
new file mode 100644
index 00000000000..6d26ba5dfa0
--- /dev/null
+++ b/spec/workers/remove_old_web_hook_logs_worker_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe RemoveOldWebHookLogsWorker do
+ subject { described_class.new }
+
+ describe '#perform' do
+ let!(:week_old_record) { create(:web_hook_log, created_at: Time.now - 1.week) }
+ let!(:three_days_old_record) { create(:web_hook_log, created_at: Time.now - 3.days) }
+ let!(:one_day_old_record) { create(:web_hook_log, created_at: Time.now - 1.day) }
+
+ it 'removes web hook logs older than 2 days' do
+ subject.perform
+
+ expect(WebHookLog.all).to include(one_day_old_record)
+ expect(WebHookLog.all).not_to include(week_old_record, three_days_old_record)
+ end
+ 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"