summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md11
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--PROCESS.md100
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js26
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es69
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es64
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es61
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es622
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js9
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss11
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss58
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/labels_controller.rb33
-rw-r--r--app/controllers/projects/mattermosts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/snippets_controller.rb6
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/models/ci/build.rb32
-rw-r--r--app/models/concerns/spammable.rb8
-rw-r--r--app/models/environment.rb18
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb14
-rw-r--r--app/models/project_services/jira_service.rb4
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb4
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/snippet.rb14
-rw-r--r--app/services/create_snippet_service.rb9
-rw-r--r--app/services/labels/promote_service.rb71
-rw-r--r--app/services/merge_requests/build_service.rb129
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/ci/status/_graph_badge.html.haml6
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/snippets/_actions.html.haml5
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_label.html.haml4
-rw-r--r--app/views/shared/_outdated_browser.html.haml3
-rw-r--r--app/views/shared/icons/_icon_action_cancel.svg1
-rw-r--r--app/views/shared/icons/_icon_action_play.svg1
-rw-r--r--app/views/shared/icons/_icon_action_retry.svg1
-rw-r--r--app/views/shared/icons/_icon_action_stop.svg1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml3
-rw-r--r--app/views/snippets/_actions.html.haml5
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--changelogs/unreleased/19164-mobile-settings.yml4
-rw-r--r--changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml4
-rw-r--r--changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml4
-rw-r--r--changelogs/unreleased/24795_refactor_merge_request_build_service.yml4
-rw-r--r--changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml4
-rw-r--r--changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml4
-rw-r--r--changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml4
-rw-r--r--changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml4
-rw-r--r--changelogs/unreleased/26852-fix-slug-for-openshift.yml4
-rw-r--r--changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml4
-rw-r--r--changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml4
-rw-r--r--changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml4
-rw-r--r--changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml4
-rw-r--r--changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml4
-rw-r--r--changelogs/unreleased/27484-environment-show-name.yml4
-rw-r--r--changelogs/unreleased/27488-fix-jwt-version.yml4
-rw-r--r--changelogs/unreleased/27494-environment-list-column-headers.yml4
-rw-r--r--changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml4
-rw-r--r--changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml4
-rw-r--r--changelogs/unreleased/cop-gem-fetcher.yml4
-rw-r--r--changelogs/unreleased/empty-selection-reply-shortcut.yml4
-rw-r--r--changelogs/unreleased/fix-26518.yml4
-rw-r--r--changelogs/unreleased/fix-27479.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-filtering-username-with-multiple-words.yml4
-rw-r--r--changelogs/unreleased/fix-import-user-validation-error.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/issue-20428.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/label-select-toggle.yml4
-rw-r--r--changelogs/unreleased/refresh-authorizations-fork-join.yml4
-rw-r--r--changelogs/unreleased/revert-filter-assigned-to-me.yml4
-rw-r--r--changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml4
-rw-r--r--changelogs/unreleased/snippet-spam.yml4
-rw-r--r--changelogs/unreleased/zj-format-chat-messages.yml4
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/routes/snippets.rb1
-rw-r--r--db/migrate/20161114024742_add_coverage_regex_to_builds.rb13
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb18
-rw-r--r--db/schema.rb1
-rw-r--r--doc/ci/autodeploy/index.md3
-rw-r--r--doc/ci/yaml/README.md38
-rw-r--r--doc/development/ux_guide/animation.md2
-rw-r--r--doc/project_services/slack.md2
-rw-r--r--features/dashboard/shortcuts.feature21
-rw-r--r--features/steps/dashboard/shortcuts.rb7
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb1
-rw-r--r--lib/gitlab/auth.rb11
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb35
-rw-r--r--lib/gitlab/chat_commands/deploy.rb24
-rw-r--r--lib/gitlab/chat_commands/help.rb28
-rw-r--r--lib/gitlab/chat_commands/issue_new.rb (renamed from lib/gitlab/chat_commands/issue_create.rb)20
-rw-r--r--lib/gitlab/chat_commands/issue_search.rb8
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb8
-rw-r--r--lib/gitlab/chat_commands/presenter.rb131
-rw-r--r--lib/gitlab/chat_commands/presenters/access.rb40
-rw-r--r--lib/gitlab/chat_commands/presenters/base.rb77
-rw-r--r--lib/gitlab/chat_commands/presenters/deploy.rb21
-rw-r--r--lib/gitlab/chat_commands/presenters/help.rb27
-rw-r--r--lib/gitlab/chat_commands/presenters/issuable.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb50
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_search.rb47
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_show.rb61
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb22
-rw-r--r--lib/gitlab/ci/config/entry/global.rb5
-rw-r--r--lib/gitlab/ci/config/entry/job.rb8
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb10
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb10
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb45
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb6
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb3
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/tasks/gitlab/import.rake2
-rw-r--r--rubocop/cop/gem_fetcher.rb28
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb45
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb4
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb84
-rw-r--r--spec/controllers/snippets_controller_spec.rb59
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb29
-rw-r--r--spec/features/environment_spec.rb4
-rw-r--r--spec/features/environments_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb15
-rw-r--r--spec/features/issues/group_label_sidebar_spec.rb21
-rw-r--r--spec/features/issues/new_branch_button_spec.rb20
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb28
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb6
-rw-r--r--spec/features/projects/new_project_spec.rb20
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb18
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb9
-rw-r--r--spec/helpers/diff_helper_spec.rb10
-rw-r--r--spec/javascripts/awards_handler_spec.js2
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js.es640
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es61
-rw-r--r--spec/javascripts/merge_request_widget_spec.js26
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js49
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb34
-rw-r--r--spec/lib/gitlab/auth_spec.rb96
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb50
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb24
-rw-r--r--spec/lib/gitlab/chat_commands/issue_new_spec.rb (renamed from spec/lib/gitlab/chat_commands/issue_create_spec.rb)14
-rw-r--r--spec/lib/gitlab/chat_commands/issue_search_spec.rb12
-rw-r--r--spec/lib/gitlab/chat_commands/issue_show_spec.rb25
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/access_spec.rb49
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb47
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb17
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb23
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/config/entry/coverage_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb24
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb41
-rw-r--r--spec/models/cycle_analytics/code_spec.rb22
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb12
-rw-r--r--spec/models/cycle_analytics/production_spec.rb18
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb18
-rw-r--r--spec/models/cycle_analytics/test_spec.rb50
-rw-r--r--spec/models/environment_spec.rb5
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb5
-rw-r--r--spec/requests/api/builds_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb8
-rw-r--r--spec/requests/api/project_snippets_spec.rb50
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/snippets_spec.rb32
-rw-r--r--spec/requests/ci/api/builds_spec.rb4
-rw-r--r--spec/services/event_create_service_spec.rb6
-rw-r--r--spec/services/labels/promote_service_spec.rb187
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb24
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb50
-rw-r--r--spec/tasks/gitlab/mail_google_schema_whitelisting.rb2
-rw-r--r--spec/workers/project_destroy_worker_spec.rb4
217 files changed, 2520 insertions, 855 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 702a4ba89a8..25e02b1ae1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.16.3 (2017-01-27)
+
+- Add caching of droplab ajax requests. !8725
+- Fix access to the wiki code via HTTP when repository feature disabled. !8758
+- Revert 3f17f29a. !8785
+- Fix race conditions for AuthorizedProjectsWorker.
+- Fix autocomplete initial undefined state.
+- Fix Error 500 when repositories contain annotated tags pointing to blobs.
+- Fix /explore sorting.
+- Fixed label dropdown toggle text not correctly updating.
+
## 8.16.2 (2017-01-25)
- allow issue filter bar to be operated with mouse only. !8681
diff --git a/Gemfile b/Gemfile
index 37a7666602f..dd7c93c5a75 100644
--- a/Gemfile
+++ b/Gemfile
@@ -36,7 +36,7 @@ gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt'
+gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 671d7788a86..3b207d19d1f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -379,7 +379,7 @@ GEM
json (1.8.3)
json-schema (2.6.2)
addressable (~> 2.3.8)
- jwt (1.5.4)
+ jwt (1.5.6)
kaminari (0.17.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -912,7 +912,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0)
jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2)
- jwt
+ jwt (~> 1.5.6)
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
@@ -1022,4 +1022,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.7
+ 1.14.2
diff --git a/PROCESS.md b/PROCESS.md
index cbeb781cd3c..993d60bbba8 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -12,106 +12,54 @@ etc.).
## Common actions
-### Issue team
-
-- Looks for issues without [workflow labels](#how-we-handle-issues) and triages
- issue
-- Closes invalid issues with a comment (duplicates,
- [fixed in newer version](#issue-fixed-in-newer-version),
- [issue report for old version](#issue-report-for-old-version), not a problem
- in GitLab, etc.)
-- Asks for feedback from issue reporter
- ([invalid issue reports](#improperly-formatted-issue),
- [format code](#code-format), etc.)
-- Monitors all issues for feedback (but especially ones commented on since
- automatically watching them)
-- Closes issues with no feedback from the reporter for two weeks
-
-### Merge marshall & merge request coach
-
-- Responds to merge requests the issue team mentions them in and monitors for
- new merge requests
-- Provides feedback to the merge request submitter to improve the merge request
- (style, tests, etc.)
-- Mark merge requests `Ready for Merge` when they meet the
- [contribution acceptance criteria]
-- Mention developer(s) based on the
- [list of members and their specialities][team]
-- Closes merge requests with no feedback from the reporter for two weeks
-
-## Priorities of the issue team
-
-1. Mentioning people (critical)
-1. Workflow labels (normal)
-1. Functional labels (minor)
-1. Assigning issues (avoid if possible)
-
-## Mentioning people
+### Issue triaging
+
+Our issue triage policies are [described in our handbook]. You are very welcome
+to help the GitLab team triage issues. We also organize [issue bash events] once
+every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
-[GitLab core team][core-team]. If there is nobody mentioned with that expertise
+[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
-## Workflow labels
+[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
+[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
-Workflow labels are purposely not very detailed since that would be hard to keep
-updated as you would need to re-evaluate them after every comment. We optionally
-use functional labels on demand when we want to group related issues to get an
-overview (for example all issues related to RVM, to tackle them in one go) and
-to add details to the issue.
+### Merge request coaching
-- ~"Awaiting Feedback" Feedback pending from the reporter
-- ~UX needs help from a UX designer
-- ~Frontend needs help from a Front-end engineer. Please follow the
- ["Implement design & UI elements" guidelines].
-- ~"Accepting Merge Requests" is a low priority, well-defined issue that we
- encourage people to contribute to. Not exclusive with other labels.
-- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
-in support or comment for further detail. Do not use `feature request`.
-- ~bug is an issue reporting undesirable or incorrect behavior.
-- ~customer is an issue reported by enterprise subscribers. This label should
-be accompanied by *bug* or *feature proposal* labels.
+Several people from the [GitLab team][team] are helping community members to get
+their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done].
-Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
+What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
+
+## Workflow labels
-## Functional labels
+Labelling issues is described in the [GitLab Inc engineering workflow].
-These labels describe what development specialities are involved such as: `CI`,
-`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`,
-`Release`, `Repository`, `UX`.
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
-## Label colors
-
-- Light orange `#fef2c0`: workflow labels for issue team members (awaiting
- feedback, awaiting confirmation of fix)
-- Bright orange `#eb6420`: workflow labels for core team members (attached MR,
- awaiting developer action/feedback)
-- Light blue `#82C5FF`: functional labels
-- Green labels `#009800`: issues that can generally be ignored. For example,
- issues given the following labels normally can be closed immediately:
- - Support (see copy & paste response:
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
-
## Be kind
Be kind to people trying to contribute. Be aware that people may be a non-native
English speaker, they might not understand things or they might be very
sensitive as to how you word things. Use Emoji to express your feelings (heart,
-star, smile, etc.). Some good tips about giving feedback to merge requests is in
-the [Thoughtbot code review guide].
+star, smile, etc.). Some good tips about code reviews can be found in our
+[Code Review Guidelines].
+
+[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze
-5 working days before the 22nd the stable branches for the upcoming release will
+On the 7th of each month, the stable branches for the upcoming release will
be frozen for major changes. Merge requests may still be merged into master
during this period. By freezing the stable branches prior to a release there's
no need to worry about last minute merge requests potentially breaking a lot of
@@ -120,10 +68,9 @@ things.
What is considered to be a major change is determined on a case by case basis as
this definition depends very much on the context of changes. For example, a 5
line change might have a big impact on the entire application. Ultimately the
-decision will be made by those reviewing a merge request and the release
-manager.
+decision will be made by the maintainers and the release managers.
-During the feature freeze all merge requests that are meant to go into the next
+During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set. Merge requests without a milestone and this label will
not be merged into any stable branches.
@@ -189,7 +136,6 @@ prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is
still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
-[core-team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 6a49715590c..a7181904ac9 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,6 +1,19 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
+ var toggleContainer = function(container, /* optional */toggleState) {
+ var $container = $(container);
+
+ $container
+ .find('.js-toggle-button .fa')
+ .toggleClass('fa-chevron-up', toggleState)
+ .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
+
+ $container
+ .find('.js-toggle-content')
+ .toggle(toggleState);
+ };
+
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
@@ -10,14 +23,7 @@
//
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
- $(this)
- .find('.fa')
- .toggleClass('fa-chevron-down fa-chevron-up')
- .end()
- .closest('.js-toggle-container')
- .find('.js-toggle-content')
- .toggle()
- ;
+ toggleContainer($(this).closest('.js-toggle-container'));
});
// If we're accessing a permalink, ensure it is not inside a
@@ -26,8 +32,8 @@
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
- if (container && container.find('.js-toggle-content').is(':hidden')) {
- container.find('.js-toggle-button').trigger('click');
+ if (container) {
+ toggleContainer(container, true);
anchor.scrollIntoView();
}
});
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index fea642467fa..971be04e2d2 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -182,7 +182,7 @@
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th>
<th class="environments-commit">Commit</th>
- <th class="environments-date">Created</th>
+ <th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
index 7bf199d9274..162fd6044e5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -39,8 +39,15 @@
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ let value = lastToken.value || '';
- return lastToken.value || '';
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
+ }
+
+ return value;
}
init() {
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 3f23095dad9..7f1f2a5d278 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -83,12 +83,12 @@
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
match = regexp.exec(subtext);
if (match) {
- return match[2] || match[1];
+ return (match[1] || match[1] === "") ? match[1] : match[2];
} else {
return null;
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 51993bb3420..e3bff2559fd 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -162,6 +162,7 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
+ if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null;
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 7cc319e2f4e..fa782ebbedf 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -154,12 +154,22 @@
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status ||
+ data.sha !== _this.opts.ci_sha ||
+ data.pipeline !== _this.opts.ci_pipeline) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
+ if (data.pipeline) {
+ _this.opts.ci_pipeline = data.pipeline;
+ _this.updatePipelineUrls(data.pipeline);
+ }
+ if (data.sha) {
+ _this.opts.ci_sha = data.sha;
+ _this.updateCommitUrls(data.sha);
+ }
if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
@@ -248,6 +258,16 @@
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
+ MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
+ const pipelineUrl = this.opts.pipeline_path;
+ $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
+ };
+
+ MergeRequestWidget.prototype.updateCommitUrls = function(id) {
+ const commitsUrl = this.opts.commits_path;
+ $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
+ };
+
return MergeRequestWidget;
})();
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 4ef516af8c8..4dcc5ebe28f 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -39,17 +39,20 @@
}
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
- var quote, replyField, documentFragment, selected, separator;
+ var quote, documentFragment, selected, separator;
+ var replyField = $('.js-main-target-form #note_note');
documentFragment = window.gl.utils.getSelectedFragment();
- if (!documentFragment) return;
+ if (!documentFragment) {
+ replyField.focus();
+ return;
+ }
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
- replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") {
return;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index bb6129158d9..cda46223492 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -330,10 +330,6 @@
}
}
-.btn-file-option {
- background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 868f28cd356..db8d231a82a 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -58,3 +58,9 @@
fill: $gl-text-color;
}
}
+
+.icon-link {
+ &:hover {
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 401c2d0f6ee..fd081c2d7e1 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -294,16 +294,18 @@
.container-fluid {
position: relative;
+
+ .nav-control {
+ @media (max-width: $screen-sm-max) {
+ margin-right: 75px;
+ }
+ }
}
.controls {
float: right;
padding: 7px 0 0;
- @media (max-width: $screen-sm-max) {
- display: none;
- }
-
i {
color: $layout-link-gray;
}
@@ -361,6 +363,7 @@
.fade-left {
@include fade(right, $gray-light);
left: -5px;
+ text-align: center;
.fa {
left: -7px;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 47dfc22d533..cf79c2e36c2 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -494,31 +494,27 @@
// Action Icons in big pipeline-graph nodes
> .ci-action-icon-container .ci-action-icon-wrapper {
- i {
- color: $border-color;
- border-radius: 100%;
- border: 1px solid $border-color;
- padding: 5px 6px;
- font-size: 13px;
- background: $white-light;
- height: 30px;
- width: 30px;
-
- &::before {
- position: relative;
- top: 3px;
- left: 3px;
- }
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
- &:hover {
- color: $gl-text-color;
- background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-bg;
- }
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
+ }
+
+ svg {
+ fill: $border-color;
+ position: relative;
+ left: -1px;
+ top: -1px;
}
- .ci-play-icon {
- padding: 5px 5px 5px 7px;
+ &:hover svg {
+ fill: $gl-text-color;
}
}
@@ -657,7 +653,7 @@
font-weight: 100;
font-size: 15px;
position: absolute;
- right: 5px;
+ right: 13px;
top: 8px;
}
@@ -825,11 +821,23 @@
&:hover,
&:focus {
- text-decoration: none;
- color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid transparent;
}
+
+ svg {
+ width: 22px;
+ height: 22px;
+ left: -6px;
+ position: relative;
+ top: -3px;
+ fill: $action-icon-color;
+ }
+
+ &:hover svg,
+ &:focus svg {
+ fill: $gl-text-color;
+ }
}
// link to the build
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 99acd98ae13..562f92bd83c 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
- redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
+ redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f81237db991..264b14713fb 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
- @group.reset_path!
+ @group.restore_path!
render action: "edit"
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 44715dd6f7a..1593b5c1afb 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -2,12 +2,13 @@ class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled
- before_action :label, only: [:edit, :update, :destroy]
+ before_action :label, only: [:edit, :update, :destroy, :promote]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
+ before_action :authorize_admin_group!, only: [:promote]
respond_to :js, :html
@@ -102,6 +103,32 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ def promote
+ promote_service = Labels::PromoteService.new(@project, @current_user)
+
+ begin
+ return render_404 unless promote_service.execute(@label)
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Label was promoted to a Group Label')
+ end
+ format.js
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
+ Gitlab::AppLogger.error e
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Failed to promote label due to internal error. Please contact administrators.')
+ end
+ format.js
+ end
+ end
+ end
+
protected
def module_enabled
@@ -129,4 +156,8 @@ class Projects::LabelsController < Projects::ApplicationController
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
+
+ def authorize_admin_group!
+ return render_404 unless can?(current_user, :admin_group, @project.group)
+ end
end
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index 01d99c7df35..38f7e6eb5e9 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -34,7 +34,7 @@ class Projects::MattermostsController < Projects::ApplicationController
end
def teams
- @teams ||= @service.list_teams(current_user)
+ @teams, @teams_error_message = @service.list_teams(current_user)
end
def service
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3492502e296..6eb542e4bd8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
- coverage: coverage
+ coverage: coverage,
+ pipeline: pipeline.try(:id)
}
render json: response
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 02a97c1c574..5d193f26a8e 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,8 +1,9 @@
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- @snippet = CreateSnippetService.new(@project, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid?
respond_with(@snippet,
@@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dee57e4a388..b169d993688 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,5 +1,6 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController
end
def create
- @snippet = CreateSnippetService.new(nil, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet)
end
@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController
end
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index c3508443d8a..311a70725ab 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -21,7 +21,7 @@ module BlobHelper
options[:link_opts])
if !on_top_of_branch?(project, ref)
- button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
@@ -32,7 +32,7 @@ module BlobHelper
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post
+ link_to "Edit", fork_path, class: 'btn', method: :post
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index e9461b9f859..6dcb624c4da 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -198,7 +198,7 @@ module CommitsHelper
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
- class: 'btn view-file js-view-file btn-file-option'
+ class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 3a83ae15dd8..fc93acfe63e 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -93,10 +93,6 @@ module VisibilityLevelHelper
current_application_settings.default_project_visibility
end
- def default_snippet_visibility
- current_application_settings.default_snippet_visibility
- end
-
def default_group_visibility
current_application_settings.default_group_visibility
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5fe8ddf69d7..b1f77bf242c 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -275,29 +275,23 @@ module Ci
end
def update_coverage
- return unless project
- coverage_regex = project.build_coverage_regex
- return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
-
- if coverage.is_a? Numeric
- update_attributes(coverage: coverage)
- end
+ update_attributes(coverage: coverage) if coverage.present?
end
def extract_coverage(text, regex)
- begin
- matches = text.scan(Regexp.new(regex)).last
- matches = matches.last if matches.kind_of?(Array)
- coverage = matches.gsub(/\d+(\.\d+)?/).first
+ return unless regex
- if coverage.present?
- coverage.to_f
- end
- rescue
- # if bad regex or something goes wrong we dont want to interrupt transition
- # so we just silentrly ignore error for now
+ matches = text.scan(Regexp.new(regex)).last
+ matches = matches.last if matches.kind_of?(Array)
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
end
+ rescue
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
end
def has_trace_file?
@@ -522,6 +516,10 @@ module Ci
self.update(artifacts_expire_at: nil)
end
+ def coverage_regex
+ super || project.try(:build_coverage_regex)
+ end
+
def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 1aa97debe42..1acff093aa1 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -34,7 +34,13 @@ module Spammable
end
def check_for_spam
- self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
+ if spam?
+ self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
+ end
+ end
+
+ def spammable_entity_type
+ self.class.name.underscore
end
def spam_title
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 652abf18a8a..577367f1eed 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,7 +1,8 @@
class Environment < ActiveRecord::Base
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
belongs_to :project, required: true, validate: true
@@ -148,17 +149,24 @@ class Environment < ActiveRecord::Base
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
self.slug = slugified
end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 2bcff541cc0..5eb1bd86e9d 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
return unless valid_token?(params[:token])
user = find_chat_user(params)
- unless user
+
+ if user
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ else
url = authorize_chat_name_url(params)
- return presenter.authorize_chat_name(url)
+ Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end
-
- Gitlab::ChatCommands::Command.new(project, user,
- params).execute
end
private
@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2ac76e97de0..80d002f9c32 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -60,9 +60,9 @@ class JiraService < IssueTrackerService
end
def help
- 'You need to configure JIRA before enabling this service. For more details
+ "You need to configure JIRA before enabling this service. For more details
read the
- [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
+ [JIRA service documentation](#{help_page_url('project_services/jira')})."
end
def title
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 50a011db74e..b0f7a42f9a3 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -28,8 +28,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
[false, e.message]
end
- def list_teams(user)
- Mattermost::Team.new(user).all
+ def list_teams(current_user)
+ [Mattermost::Team.new(current_user).all, nil]
rescue Mattermost::Error => e
[[], e.message]
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 25b5d777641..9bb456eee24 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
+
+ def check_for_spam?
+ super && project.public?
+ end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 771a7350556..2665a7249a3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable
include Awardable
include Mentionable
+ include Spammable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
end
- default_value_for :visibility_level, Snippet::PRIVATE
+ default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
belongs_to :author, class_name: 'User'
belongs_to :project
@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base
participant :author
participant :notes_with_associations
+ attr_spammable :title, spam_title: true
+ attr_spammable :content, spam_description: true
+
def self.reference_prefix
'$'
end
@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base
notes.includes(:author)
end
+ def check_for_spam?
+ public?
+ end
+
+ def spammable_entity_type
+ 'snippet'
+ end
+
class << self
# Searches for snippets with a matching title or file name.
#
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 95cc9baf406..14f5ba064ff 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,5 +1,8 @@
class CreateSnippetService < BaseService
def execute
+ request = params.delete(:request)
+ api = params.delete(:api)
+
snippet = if project
project.snippets.build(params)
else
@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
+ snippet.spam = SpamService.new(snippet, request).check(api)
+
+ if snippet.save
+ UserAgentDetailService.new(snippet, request).create
+ end
- snippet.save
snippet
end
end
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
new file mode 100644
index 00000000000..76d0ba67b07
--- /dev/null
+++ b/app/services/labels/promote_service.rb
@@ -0,0 +1,71 @@
+module Labels
+ class PromoteService < BaseService
+ BATCH_SIZE = 1000
+
+ def execute(label)
+ return unless project.group &&
+ label.is_a?(ProjectLabel)
+
+ Label.transaction do
+ new_label = clone_label_to_group_label(label)
+
+ label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids|
+ update_issuables(new_label, batched_ids)
+ update_issue_board_lists(new_label, batched_ids)
+ update_priorities(new_label, batched_ids)
+ # Order is important, project labels need to be last
+ update_project_labels(batched_ids)
+ end
+
+ # We skipped validations during creation. Let's run them now, after deleting conflicting labels
+ raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+ new_label
+ end
+ end
+
+ private
+
+ def label_ids_for_merge(new_label)
+ LabelsFinder.
+ new(current_user, title: new_label.title, group_id: project.group.id).
+ execute(skip_authorization: true).
+ where.not(id: new_label).
+ select(:id) # Can't use pluck() to avoid object-creation because of the batching
+ end
+
+ def update_issuables(new_label, label_ids)
+ LabelLink.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_issue_board_lists(new_label, label_ids)
+ List.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_priorities(new_label, label_ids)
+ LabelPriority.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_project_labels(label_ids)
+ Label.where(id: label_ids).delete_all
+ end
+
+ def clone_label_to_group_label(label)
+ params = label.attributes.slice('title', 'description', 'color')
+ # Since the title of the new label has to be the same as the previous labels
+ # and we're merging old labels in batches we'll skip validation to omit 2-step
+ # merge process and do it in one batch
+ # We'll be forcing validation at the end of the transaction to ensure everything
+ # was merged correctly
+ new_label = GroupLabel.new(params.merge(group: project.group))
+ new_label.save(validate: false)
+
+ new_label
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 6a7393a9921..1d6d2754559 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,62 +1,88 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
- merge_request = MergeRequest.new(params)
-
- # Set MR attributes
- merge_request.can_be_created = true
+ self.merge_request = MergeRequest.new(params)
+ merge_request.can_be_created = true
merge_request.compare_commits = []
- merge_request.source_project = project unless merge_request.source_project
+ merge_request.source_project = find_source_project
+ merge_request.target_project = find_target_project
+ merge_request.target_branch = find_target_branch
+
+ if branches_specified? && branches_valid?
+ compare_branches
+ assign_title_and_description
+ else
+ merge_request.can_be_created = false
+ end
+
+ merge_request
+ end
- merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project)
+ private
- merge_request.target_project ||= (project.forked_from_project || project)
- merge_request.target_branch ||= merge_request.target_project.default_branch
+ attr_accessor :merge_request
- messages = validate_branches(merge_request)
- return build_failed(merge_request, messages) unless messages.empty?
+ delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
+
+ def find_source_project
+ source_project || project
+ end
+ def find_target_project
+ return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+ project.forked_from_project || project
+ end
+
+ def find_target_branch
+ target_branch || target_project.default_branch
+ end
+
+ def branches_specified?
+ params[:source_branch] && params[:target_branch]
+ end
+
+ def branches_valid?
+ validate_branches
+ errors.blank?
+ end
+
+ def compare_branches
compare = CompareService.new.execute(
- merge_request.source_project,
- merge_request.source_branch,
- merge_request.target_project,
- merge_request.target_branch,
+ source_project,
+ source_branch,
+ target_project,
+ target_branch
)
merge_request.compare_commits = compare.commits
merge_request.compare = compare
-
- set_title_and_description(merge_request)
end
- private
-
- def validate_branches(merge_request)
- messages = []
-
- if merge_request.target_branch.blank? || merge_request.source_branch.blank?
- messages <<
- if params[:source_branch] || params[:target_branch]
- "You must select source and target branch"
- end
- end
+ def validate_branches
+ add_error('You must select source and target branch') unless branches_present?
+ add_error('You must select different branches') if same_source_and_target?
+ add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
+ add_error("Target branch \"#{target_branch}\" does not exist") unless target_branch_exists?
+ end
- if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ def add_error(message)
+ errors.add(:base, message)
+ end
- messages << 'You must select different branches'
- end
+ def branches_present?
+ target_branch.present? && source_branch.present?
+ end
- # See if source and target branches exist
- if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
- messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
- end
+ def same_source_and_target?
+ source_project == target_project && target_branch == source_branch
+ end
- if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
- messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
- end
+ def source_branch_exists?
+ source_branch.blank? || source_project.commit(source_branch)
+ end
- messages
+ def target_branch_exists?
+ target_branch.blank? || target_project.commit(target_branch)
end
# When your branch name starts with an iid followed by a dash this pattern will be
@@ -71,17 +97,17 @@ module MergeRequests
# - Setting the title as 'Resolves "Emoji don't show up in commit title"' if there is
# more than one commit in the MR
#
- def set_title_and_description(merge_request)
- if match = merge_request.source_branch.match(/\A(\d+)-/)
+ def assign_title_and_description
+ if match = source_branch.match(/\A(\d+)-/)
iid = match[1]
end
- commits = merge_request.compare_commits
+ commits = compare_commits
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
+ elsif iid && issue = target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
@@ -89,31 +115,20 @@ module MergeRequests
merge_request.title = "Resolve #{issue.title}"
end
else
- merge_request.title = merge_request.source_branch.titleize.humanize
+ merge_request.title = source_branch.titleize.humanize
end
if iid
closes_issue = "Closes ##{iid}"
- if merge_request.description.present?
+ if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
end
- merge_request.title = merge_request.wip_title if commits.empty?
-
- merge_request
- end
-
- def build_failed(merge_request, messages)
- messages.compact.each do |message|
- merge_request.errors.add(:base, message)
- end
- merge_request.compare_commits = []
- merge_request.can_be_created = false
- merge_request
+ merge_request.title = wip_title if commits.empty?
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f74e6cac174..b2cc39763f3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -365,7 +365,7 @@ class NotificationService
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users)
+ users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
@@ -415,8 +415,8 @@ class NotificationService
end
# Build a list of users based on group notification settings
- def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(project, :watch)
+ def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
+ uids = notification_settings_for(group, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -473,7 +473,7 @@ class NotificationService
setting = user.notification_settings_for(project)
- if !setting && project.group
+ if project.group && (setting.nil? || setting.global?)
setting = user.notification_settings_for(project.group)
end
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 8dea3479f82..8ed23ac4919 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,4 +16,4 @@
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
- = icon(status.action_icon, class: status.action_class)
+ = custom_icon(status.action_icon)
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index dd2f649de9a..0530d21a7e2 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -2,7 +2,7 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
-- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- klass = "ci-status-icon ci-status-icon-#{status.group} js-ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
@@ -16,5 +16,5 @@
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
- %i.ci-action-icon-wrapper
- = icon(status.action_icon, class: status.action_class)
+ %i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" }
+ = custom_icon(status.action_icon)
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac04f57e217..19a947af4ca 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,5 +1,5 @@
+= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class }
- = render 'layouts/nav/admin_settings'
.fade-left
= icon('angle-left')
.fade-right
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 6dba42d5226..4d0b7a5ca85 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -63,9 +63,10 @@
- if @commit.status
.well-segment.pipeline-info
%div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = ci_icon_for_status(@commit.status)
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+ = ci_icon_for_status(@commit.status)
Pipeline
- = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace"
+ = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
for
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
%span.ci-status-label
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index c37a33bbcd5..fc478ccc995 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,7 +5,7 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
- if editable_diff?(diff_file)
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 6e0d9456900..f3179dce5f2 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
.top-area.adjust
.col-md-9
- %h3.page-title= @environment.name.capitalize
+ %h3.page-title= @environment.name
.col-md-3
.nav-controls
= render 'projects/environments/terminal_button', environment: @environment
@@ -33,7 +33,7 @@
%th ID
%th Commit
%th Build
- %th
+ %th Created
%th.hidden-xs
= render @deployments
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9fa00811af0..11636d7ebc7 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -75,7 +75,7 @@
// This element is filled in using JavaScript.
.content-block.content-block-small
- = render 'new_branch'
+ = render 'new_branch' unless @issue.confidential?
= render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 605c7f61dee..aac74a25b75 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -1,3 +1,7 @@
+- if @teams_error_message
+ = content_for :flash_message do
+ .alert.alert-danger= @teams_error_message
+
%p
You aren’t a member of any team on the Mattermost instance at
%strong= Gitlab.config.mattermost.host
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
index 2595ce74ac0..0839880713f 100644
--- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
- %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" }
+ %a.btn.view-file{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 5faa6c43f9f..0e3af62ebc2 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -2,14 +2,15 @@
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = ci_icon_for_status(status)
+ = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
+ = ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
= succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 38328501ffd..f07e6b3ad54 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -24,6 +24,10 @@
preparing: "{{status}} build",
normal: "Build {{status}}"
},
+ ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
+ ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
+ commits_path: "#{project_commits_path(@project)}",
+ pipeline_path: "#{project_pipelines_path(@project)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index ca76f13ef5e..6caa5f16dc6 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -23,8 +23,8 @@
.info-well
- if @commit.status
.well-segment.pipeline-info
- %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = ci_icon_for_status(@commit.status)
+ .icon-container
+ = icon('clock-o')
= pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref
from
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 068a6610350..e2a5107a883 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -8,6 +8,8 @@
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -27,3 +29,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 216f70f5605..fb39028529d 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
Edit Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
+= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet)
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 772a594269c..cfed3a79bc5 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 65a3a6bddab..54b5ae2402e 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
.well.prepend-top-20
%ul
@@ -13,4 +13,4 @@
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
- To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
+ To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 4bfdf94b913..ead9b84b991 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -66,6 +66,10 @@
%a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
Group level
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_group, label.project.group)
+ = link_to promote_namespace_project_label_path(label.project.namespace, label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do
+ %span.sr-only Promote to Group
+ = icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 0eba1fe075f..c06d1ffa59b 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,8 +1,7 @@
- if outdated_browser?
- - link = "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/requirements.md#supported-web-browsers"
.browser-alert
GitLab may not work properly because you are using an outdated web browser.
%br
Please install a
- = link_to 'supported web browser', link
+ = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
for a better experience.
diff --git a/app/views/shared/icons/_icon_action_cancel.svg b/app/views/shared/icons/_icon_action_cancel.svg
new file mode 100644
index 00000000000..a1f700eb0ff
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_cancel.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M19.25,14.9765625 C19.25,14.1380166 19.0234398,13.3697952 18.5703125,12.671875 L12.6796875,18.5546875 C13.3932327,19.0182315 14.1666625,19.25 15,19.25 C15.5781279,19.25 16.1289036,19.1367199 16.6523438,18.9101562 C17.1757839,18.6835926 17.6276023,18.3802102 18.0078125,18 C18.3880227,17.6197898 18.690103,17.1653672 18.9140625,16.6367188 C19.138022,16.1080703 19.25,15.5546904 19.25,14.9765625 Z M11.4453125,17.3125 L17.34375,11.421875 C16.6406215,10.9479143 15.8593793,10.7109375 15,10.7109375 C14.2291628,10.7109375 13.5182324,10.9010398 12.8671875,11.28125 C12.2161426,11.6614602 11.7005227,12.1796842 11.3203125,12.8359375 C10.9401023,13.4921908 10.75,14.2057253 10.75,14.9765625 C10.75,15.8203167 10.9817685,16.5989548 11.4453125,17.3125 Z M21,14.9765625 C21,15.7942749 20.8411474,16.5755171 20.5234375,17.3203125 C20.2057276,18.0651079 19.7799506,18.7057265 19.2460938,19.2421875 C18.7122369,19.7786485 18.0742225,20.2057276 17.3320312,20.5234375 C16.58984,20.8411474 15.8125041,21 15,21 C14.1874959,21 13.41016,20.8411474 12.6679688,20.5234375 C11.9257775,20.2057276 11.2877631,19.7786485 10.7539062,19.2421875 C10.2200494,18.7057265 9.79427242,18.0651079 9.4765625,17.3203125 C9.15885258,16.5755171 9,15.7942749 9,14.9765625 C9,14.1588501 9.15885258,13.37891 9.4765625,12.6367188 C9.79427242,11.8945275 10.2200494,11.255211 10.7539062,10.71875 C11.2877631,10.182289 11.9257775,9.75520992 12.6679688,9.4375 C13.41016,9.11979008 14.1874959,8.9609375 15,8.9609375 C15.8125041,8.9609375 16.58984,9.11979008 17.3320312,9.4375 C18.0742225,9.75520992 18.7122369,10.182289 19.2460938,10.71875 C19.7799506,11.255211 20.2057276,11.8945275 20.5234375,12.6367188 C20.8411474,13.37891 21,14.1588501 21,14.9765625 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_play.svg b/app/views/shared/icons/_icon_action_play.svg
new file mode 100644
index 00000000000..6ac192cd7e9
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_play.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M21.5401786,15.2320328 L11.90625,20.5858274 C11.7950143,20.6486998 11.6994982,20.6559541 11.6196987,20.6075908 C11.5398992,20.5592275 11.5,20.4721748 11.5,20.3464301 L11.5,9.66785867 C11.5,9.54211399 11.5398992,9.45506129 11.6196987,9.40669795 C11.6994982,9.35833462 11.7950143,9.36558901 11.90625,9.42846135 L21.5401786,14.782256 C21.6514142,14.8451283 21.7070312,14.9200904 21.7070312,15.0071444 C21.7070312,15.0941984 21.6514142,15.1691604 21.5401786,15.2320328 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_retry.svg b/app/views/shared/icons/_icon_action_retry.svg
new file mode 100644
index 00000000000..0fa0243f3c0
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_retry.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M20.6114971,16.0821413 C20.6114971,16.106323 20.6090789,16.1232499 20.6042426,16.1329226 C20.2947172,17.42906 19.6466582,18.4797378 18.6600462,19.2849873 C17.6734341,20.0902369 16.5175677,20.4928556 15.1924122,20.4928556 C14.4863075,20.4928556 13.8031856,20.3598584 13.1430261,20.0938601 C12.4828665,19.8278617 11.8940517,19.4482152 11.376564,18.9549092 L10.4407381,19.8907351 C10.3488478,19.9826254 10.2400319,20.0285699 10.1142872,20.0285699 C9.98854256,20.0285699 9.87972669,19.9826254 9.78783635,19.8907351 C9.69594601,19.7988447 9.65000153,19.6900289 9.65000153,19.5642842 L9.65000153,16.3142842 C9.65000153,16.1885395 9.69594601,16.0797236 9.78783635,15.9878333 C9.87972669,15.895943 9.98854256,15.8499985 10.1142872,15.8499985 L13.3642872,15.8499985 C13.4900319,15.8499985 13.5988478,15.895943 13.6907381,15.9878333 C13.7826285,16.0797236 13.828573,16.1885395 13.828573,16.3142842 C13.828573,16.4400289 13.7826285,16.5488447 13.6907381,16.6407351 L12.6968765,17.6345967 C13.0402562,17.9537947 13.4295752,18.200444 13.8648453,18.374552 C14.3001153,18.5486601 14.7523057,18.6357128 15.2214301,18.6357128 C15.8694988,18.6357128 16.4740315,18.4785343 17.0350462,18.1641726 C17.5960609,17.8498109 18.0458332,17.4169655 18.3843765,16.8656235 C18.4375762,16.7834058 18.5657371,16.5004845 18.7688631,16.0168512 C18.8075538,15.9056155 18.8800977,15.8499985 18.9864971,15.8499985 L20.3793542,15.8499985 C20.4422265,15.8499985 20.4966345,15.8729707 20.5425797,15.9189159 C20.5885248,15.9648611 20.6114971,16.019269 20.6114971,16.0821413 Z M20.7928587,10.2785699 L20.7928587,13.5285699 C20.7928587,13.6543146 20.7469142,13.7631305 20.6550238,13.8550208 C20.5631335,13.9469111 20.4543176,13.9928556 20.328573,13.9928556 L17.078573,13.9928556 C16.9528283,13.9928556 16.8440124,13.9469111 16.7521221,13.8550208 C16.6602317,13.7631305 16.6142872,13.6543146 16.6142872,13.5285699 C16.6142872,13.4028252 16.6602317,13.2940094 16.7521221,13.202119 L17.7532381,12.2010029 C17.0374607,11.5384252 16.1935332,11.2071413 15.2214301,11.2071413 C14.5733614,11.2071413 13.9688287,11.3643198 13.407814,11.6786815 C12.8467993,11.9930432 12.397027,12.4258886 12.0584837,12.9772306 C12.005284,13.0594483 11.8771231,13.3423696 11.6739971,13.8260029 C11.6353064,13.9372386 11.5627625,13.9928556 11.4563631,13.9928556 L10.0127247,13.9928556 C9.9498524,13.9928556 9.89544446,13.9698834 9.84949929,13.9239382 C9.80355412,13.877993 9.78058188,13.8235851 9.78058188,13.7607128 L9.78058188,13.7099315 C10.0949436,12.4137941 10.7478388,11.3631163 11.7392872,10.5578668 C12.7307356,9.75261722 13.8914383,9.34999847 15.2214301,9.34999847 C15.9275348,9.34999847 16.6142839,9.48420472 17.281698,9.75262124 C17.949112,10.0210378 18.541554,10.3994752 19.0590417,10.8879449 L20.0021221,9.95211901 C20.0940124,9.86022867 20.2028283,9.81428419 20.328573,9.81428419 C20.4543176,9.81428419 20.5631335,9.86022867 20.6550238,9.95211901 C20.7469142,10.0440094 20.7928587,10.1528252 20.7928587,10.2785699 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_stop.svg b/app/views/shared/icons/_icon_action_stop.svg
new file mode 100644
index 00000000000..1c8e2fe4156
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_stop.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M20.1357204,10.2985704 L20.1357204,19.7271418 C20.1357204,19.8432138 20.0933101,19.9436592 20.0084882,20.0284811 C19.9236664,20.1133029 19.823221,20.1557132 19.707149,20.1557132 L10.2785775,20.1557132 C10.1625055,20.1557132 10.0620601,20.1133029 9.97723825,20.0284811 C9.89241639,19.9436592 9.8500061,19.8432138 9.8500061,19.7271418 L9.8500061,10.2985704 C9.8500061,10.1824984 9.89241639,10.0820529 9.97723825,9.99723107 C10.0620601,9.91240922 10.1625055,9.86999893 10.2785775,9.86999893 L19.707149,9.86999893 C19.823221,9.86999893 19.9236664,9.91240922 20.0084882,9.99723107 C20.0933101,10.0820529 20.1357204,10.1824984 20.1357204,10.2985704 Z"></path></svg>
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ec9bcaf63dd..10fa7901874 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -130,7 +130,7 @@
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
- = link_to_label(label, type: issuable.to_ability_name)
+ = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
- else
%span.no-value None
.selectbox.hide-collapsed
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b5c0a7fd6d4..a736bfd91e2 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
%p
Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
= succeed "." do
- %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails
+ %a{ href: help_page_path('workflow/notifications'), target: "_blank" } notification emails
.col-lg-8
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 0c788032020..2d22782eb36 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -11,7 +11,7 @@
.col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true
- = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
+ = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor
.form-group
@@ -34,4 +34,3 @@
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 95fc7198104..9a9a3ff9220 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -8,6 +8,8 @@
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -26,3 +28,6 @@
%li
= link_to edit_snippet_path(@snippet) do
Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 82f44a9a5c3..915bf98eb3e 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
Edit Snippet
%hr
-= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
+= render 'shared/snippets/form', url: snippet_path(@snippet)
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 79e2392490d..ca8afb4bb6a 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: snippets_path(@snippet)
diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml
new file mode 100644
index 00000000000..c26a20f87e2
--- /dev/null
+++ b/changelogs/unreleased/19164-mobile-settings.yml
@@ -0,0 +1,4 @@
+---
+title: 19164 Add settings dropdown to mobile screens
+merge_request:
+author:
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
new file mode 100644
index 00000000000..587ef4f9a73
--- /dev/null
+++ b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
@@ -0,0 +1,4 @@
+---
+title: Fix disable storing of sensitive information when importing a new repo
+merge_request: 8885
+author: Bernard Pietraga
diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
new file mode 100644
index 00000000000..05fbd8f0bf2
--- /dev/null
+++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
+merge_request: 8752
+author:
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
new file mode 100644
index 00000000000..b735fb57649
--- /dev/null
+++ b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor MergeRequests::BuildService
+merge_request: 8462
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
new file mode 100644
index 00000000000..f74e9fa8b6d
--- /dev/null
+++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
@@ -0,0 +1,4 @@
+---
+title: Update pipeline and commit links when CI status is updated
+merge_request: 8351
+author:
diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
new file mode 100644
index 00000000000..9506692dd40
--- /dev/null
+++ b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
@@ -0,0 +1,4 @@
+---
+title: Convert pipeline action icons to svg to have them propperly positioned
+merge_request:
+author:
diff --git a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml b/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml
deleted file mode 100644
index 2d4ec482ee0..00000000000
--- a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix autocomplete initial undefined state
-merge_request:
-author:
diff --git a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml b/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml
deleted file mode 100644
index 4678297cfd4..00000000000
--- a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add caching of droplab ajax requests
-merge_request: 8725
-author:
diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
new file mode 100644
index 00000000000..fb65b068b23
--- /dev/null
+++ b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG
+merge_request: 8638
+author:
diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
new file mode 100644
index 00000000000..c5c57af5aaf
--- /dev/null
+++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
@@ -0,0 +1,4 @@
+---
+title: Improve pipeline status icon linking in widgets
+merge_request:
+author:
diff --git a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml b/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml
deleted file mode 100644
index 0f0a8940f72..00000000000
--- a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix /explore sorting
-merge_request:
-author:
diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml
new file mode 100644
index 00000000000..1758ed9e9ea
--- /dev/null
+++ b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml
@@ -0,0 +1,4 @@
+---
+title: Support non-ASCII characters in GFM autocomplete
+merge_request: 8729
+author:
diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml
new file mode 100644
index 00000000000..ddd454da376
--- /dev/null
+++ b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml
@@ -0,0 +1,4 @@
+---
+title: Fix permalink discussion note being collapsed
+merge_request:
+author:
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
new file mode 100644
index 00000000000..293aab67d39
--- /dev/null
+++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Unify MR diff file button style
+merge_request: 8874
+author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
new file mode 100644
index 00000000000..dc400d65006
--- /dev/null
+++ b/changelogs/unreleased/27484-environment-show-name.yml
@@ -0,0 +1,4 @@
+---
+title: Don't capitalize environment name in show page
+merge_request:
+author:
diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml
new file mode 100644
index 00000000000..5135ff0fd60
--- /dev/null
+++ b/changelogs/unreleased/27488-fix-jwt-version.yml
@@ -0,0 +1,4 @@
+---
+title: Update and pin the `jwt` gem to ~> 1.5.6
+merge_request:
+author:
diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml
new file mode 100644
index 00000000000..798c01f3238
--- /dev/null
+++ b/changelogs/unreleased/27494-environment-list-column-headers.yml
@@ -0,0 +1,4 @@
+---
+title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
+merge_request:
+author:
diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
new file mode 100644
index 00000000000..bc990c66866
--- /dev/null
+++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong call to ProjectCacheWorker.perform
+merge_request: 8910
+author:
diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
new file mode 100644
index 00000000000..11d1f55172b
--- /dev/null
+++ b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
@@ -0,0 +1,4 @@
+---
+title: Fix notifications when set at group level
+merge_request: 6813
+author: Alexandre Maia
diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml
new file mode 100644
index 00000000000..506815a5b54
--- /dev/null
+++ b/changelogs/unreleased/cop-gem-fetcher.yml
@@ -0,0 +1,4 @@
+---
+title: Cop for gem fetched from a git source
+merge_request: 8856
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
new file mode 100644
index 00000000000..5a42c98a800
--- /dev/null
+++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml
@@ -0,0 +1,4 @@
+---
+title: Change the reply shortcut to focus the field even without a selection.
+merge_request: 8873
+author: Brian Hall
diff --git a/changelogs/unreleased/fix-26518.yml b/changelogs/unreleased/fix-26518.yml
deleted file mode 100644
index 961ac2642fb..00000000000
--- a/changelogs/unreleased/fix-26518.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix access to the wiki code via HTTP when repository feature disabled
-merge_request: 8758
-author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
new file mode 100644
index 00000000000..cc72a830695
--- /dev/null
+++ b/changelogs/unreleased/fix-27479.yml
@@ -0,0 +1,4 @@
+---
+title: Remove new branch button for confidential issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml
new file mode 100644
index 00000000000..61817027720
--- /dev/null
+++ b/changelogs/unreleased/fix-depr-warn.yml
@@ -0,0 +1,4 @@
+---
+title: resolve deprecation warnings
+merge_request: 8855
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml
new file mode 100644
index 00000000000..3513f5afdfb
--- /dev/null
+++ b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml
@@ -0,0 +1,4 @@
+---
+title: Fix filtering usernames with multiple words
+merge_request: 8851
+author:
diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml
new file mode 100644
index 00000000000..985a3b0b26f
--- /dev/null
+++ b/changelogs/unreleased/fix-import-user-validation-error.yml
@@ -0,0 +1,4 @@
+---
+title: Remove old project members when retrying an export
+merge_request:
+author:
diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml
new file mode 100644
index 00000000000..c11c2d4ede1
--- /dev/null
+++ b/changelogs/unreleased/group-label-sidebar-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed group label links in issue/merge request sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
new file mode 100644
index 00000000000..60da1c14702
--- /dev/null
+++ b/changelogs/unreleased/issue-20428.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to define a coverage regex in the .gitlab-ci.yml
+merge_request: 7447
+author: Leandro Camargo
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
new file mode 100644
index 00000000000..2ab997bf420
--- /dev/null
+++ b/changelogs/unreleased/label-promotion.yml
@@ -0,0 +1,4 @@
+---
+title: "Project labels can now be promoted to group labels"
+merge_request: 7242
+author: Olaf Tomalka
diff --git a/changelogs/unreleased/label-select-toggle.yml b/changelogs/unreleased/label-select-toggle.yml
deleted file mode 100644
index af5b4246521..00000000000
--- a/changelogs/unreleased/label-select-toggle.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed label dropdown toggle text not correctly updating
-merge_request:
-author:
diff --git a/changelogs/unreleased/refresh-authorizations-fork-join.yml b/changelogs/unreleased/refresh-authorizations-fork-join.yml
deleted file mode 100644
index b1349b9447d..00000000000
--- a/changelogs/unreleased/refresh-authorizations-fork-join.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix race conditions for AuthorizedProjectsWorker
-merge_request:
-author:
diff --git a/changelogs/unreleased/revert-filter-assigned-to-me.yml b/changelogs/unreleased/revert-filter-assigned-to-me.yml
deleted file mode 100644
index 37f9d2f5fc4..00000000000
--- a/changelogs/unreleased/revert-filter-assigned-to-me.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Revert 3f17f29a
-merge_request: 8785
-author:
diff --git a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml b/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml
deleted file mode 100644
index ff2b38f21f2..00000000000
--- a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Error 500 when repositories contain annotated tags pointing to blobs
-merge_request:
-author:
diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml
new file mode 100644
index 00000000000..4867f088953
--- /dev/null
+++ b/changelogs/unreleased/snippet-spam.yml
@@ -0,0 +1,4 @@
+---
+title: Check public snippets for spam
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
new file mode 100644
index 00000000000..2494884f5c9
--- /dev/null
+++ b/changelogs/unreleased/zj-format-chat-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Reformat messages ChatOps
+merge_request: 8528
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 6620b765e02..efe2fbc521d 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
+ post :mark_as_spam
end
end
@@ -220,6 +221,7 @@ constraints(ProjectUrlConstrainer.new) do
end
member do
+ post :promote
post :toggle_subscription
delete :remove_priority
end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 3ca096f31ba..ce0d1314292 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do
member do
get 'raw'
get 'download'
+ post :mark_as_spam
end
end
diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
new file mode 100644
index 00000000000..88aa5d52b39
--- /dev/null
+++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCoverageRegexToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :coverage_regex, :string
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
index 7153e6a32b1..8e98ee5b9ba 100644
--- a/db/migrate/20161207231626_add_environment_slug.rb
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -8,8 +8,9 @@ class AddEnvironmentSlug < ActiveRecord::Migration
DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
def up
environments = Arel::Table.new(:environments)
@@ -39,17 +40,24 @@ class AddEnvironmentSlug < ActiveRecord::Migration
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
slugified
end
diff --git a/db/schema.rb b/db/schema.rb
index 5efb4f6595c..c73c311ccb2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.datetime "queued_at"
t.string "token"
t.integer "lock_version"
+ t.string "coverage_regex"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 630207ffa09..c4c4d95b68a 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,6 +1,6 @@
# Auto deploy
-> [Introduced][mr-8135] in GitLab 8.15.
+> [Introduced][mr-8135] in GitLab 8.15. Currently requires a [Public project][project-settings].
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
@@ -33,6 +33,7 @@ enable [Kubernetes service][kubernetes-service].
created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
+[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
[project-services]: ../../project_services/project_services.md
[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
[kubernetes-service]: ../../project_services/kubernetes.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index f11257be5c3..06810898cfe 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables |
| cache | no | Define list of files that should be cached between subsequent runs |
+| coverage | no | Define coverage settings for all jobs |
### image and services
@@ -278,6 +279,23 @@ cache:
untracked: true
```
+### coverage
+
+`coverage` allows you to configure how coverage will be filtered out from the
+build outputs. Setting this up globally will make all the jobs to use this
+setting for output filtering and extracting the coverage information from your
+builds.
+
+Regular expressions are the only valid kind of value expected here. So, using
+surrounding `/` is mandatory in order to consistently and explicitly represent
+a regular expression string. You must escape special characters if you want to
+match them literally.
+
+A simple example:
+```yaml
+coverage: /\(\d+\.\d+\) covered\./
+```
+
## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
@@ -319,6 +337,7 @@ job_name:
| before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build |
| environment | no | Defines a name of environment to which deployment is done by this build |
+| coverage | no | Define coverage settings for a given job |
### script
@@ -993,6 +1012,25 @@ job:
- execute this after my script
```
+### job coverage
+
+This entry is pretty much the same as described in the global context in
+[`coverage`](#coverage). The only difference is that, by setting it inside
+the job level, whatever is set in there will take precedence over what has
+been defined in the global level. A quick example of one overriding the
+other would be:
+
+```yaml
+coverage: /\(\d+\.\d+\) covered\./
+
+job1:
+ coverage: /Code coverage: \d+\.\d+/
+```
+
+In the example above, considering the context of the job `job1`, the coverage
+regex that would be used is `/Code coverage: \d+\.\d+/` instead of
+`/\(\d+\.\d+\) covered\./`.
+
## Git Strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
index 903e54bf9dc..5dae4bcc905 100644
--- a/doc/development/ux_guide/animation.md
+++ b/doc/development/ux_guide/animation.md
@@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net](
### Hover
-Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect.
+Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect.
View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 0b682b43810..eaceb2be137 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -15,7 +15,7 @@ Slack:
After you set up Slack, it's time to set up GitLab.
-Go to your project's **Settings > Services > Slack Notifications** and you will see a
+Go to your project's **Settings > Integrations > Slack Notifications** and you will see a
checkbox with the following events that can be triggered:
- Push
diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature
deleted file mode 100644
index 41d79aa6ec8..00000000000
--- a/features/dashboard/shortcuts.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-@dashboard
-Feature: Dashboard Shortcuts
- Background:
- Given I sign in as a user
- And I visit dashboard page
-
- @javascript
- Scenario: Navigate to projects tab
- Given I press "g" and "p"
- Then the active main tab should be Projects
-
- @javascript
- Scenario: Navigate to issue tab
- Given I press "g" and "i"
- Then the active main tab should be Issues
-
- @javascript
- Scenario: Navigate to merge requests tab
- Given I press "g" and "m"
- Then the active main tab should be Merge Requests
-
diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb
deleted file mode 100644
index 118d27888df..00000000000
--- a/features/steps/dashboard/shortcuts.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedSidebarActiveTab
- include SharedShortcuts
-end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 9d8c5b63685..dcc0c82ee27 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -58,7 +58,7 @@ module API
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
- snippet_params = declared_params
+ snippet_params = declared_params.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code)
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index e096e636806..eb9ece49e7f 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -64,7 +64,7 @@ module API
desc: 'The visibility level of the snippet'
end
post do
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted?
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 7463bd719d5..649ee4d018b 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -61,6 +61,7 @@ module Ci
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
environment: job[:environment_name],
+ coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8dda65c71ef..f638905a1e0 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,16 @@ module Gitlab
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
+ # `user_with_password_for_git` should be the last check
+ # because it's the most expensive, especially when LDAP
+ # is enabled.
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
lfs_token_check(login, password) ||
+ oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) ||
+ user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
@@ -143,7 +146,9 @@ module Gitlab
read_authentication_abilities
end
- Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+ if Devise.secure_compare(token_handler.token, password)
+ Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
+ end
end
def build_access_token_check(login, password)
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 4fe53ce93a9..25da8474e95 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index 145086755e4..f34ed0f4cf2 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
- Gitlab::ChatCommands::IssueCreate,
+ Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy,
].freeze
@@ -13,51 +13,32 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- present command.new(project, current_user, params).execute(match)
+ command.new(project, current_user, params).execute(match)
else
- access_denied
+ Gitlab::ChatCommands::Presenters::Access.new.access_denied
end
else
- help(help_messages)
+ Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end
end
def match_command
match = nil
- service = available_commands.find do |klass|
- match = klass.match(command)
- end
+ service =
+ available_commands.find do |klass|
+ match = klass.match(params[:text])
+ end
[service, match]
end
private
- def help_messages
- available_commands.map(&:help_message)
- end
-
def available_commands
COMMANDS.select do |klass|
klass.available?(project)
end
end
-
- def command
- params[:text]
- end
-
- def help(messages)
- presenter.help(messages, params[:command])
- end
-
- def access_denied
- presenter.access_denied
- end
-
- def present(resource)
- presenter.present(resource)
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 7127d2f6d04..458d90f84e8 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -1,8 +1,6 @@
module Gitlab
module ChatCommands
class Deploy < BaseCommand
- include Gitlab::Routing.url_helpers
-
def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
@@ -24,35 +22,29 @@ module Gitlab
to = match[:to]
actions = find_actions(from, to)
- return unless actions.present?
- if actions.one?
- play!(from, to, actions.first)
+ if actions.none?
+ Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
+ elsif actions.one?
+ action = play!(from, to, actions.first)
+ Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else
- Result.new(:error, 'Too many actions defined')
+ Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end
end
private
def play!(from, to, action)
- new_action = action.play(current_user)
-
- Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
+ action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
- return unless environment
+ return [] unless environment
environment.actions_for(to).select(&:starts_environment?)
end
-
- def url(subject)
- polymorphic_url(
- [subject.project.namespace.becomes(Namespace), subject.project, subject]
- )
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb
new file mode 100644
index 00000000000..6c0e4d304a4
--- /dev/null
+++ b/lib/gitlab/chat_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module ChatCommands
+ class Help < BaseCommand
+ # This class has to be used last, as it always matches. It has to match
+ # because other commands were not triggered and we want to show the help
+ # command
+ def self.match(_text)
+ true
+ end
+
+ def self.help_message
+ 'help'
+ end
+
+ def self.allowed?(_project, _user)
+ true
+ end
+
+ def execute(commands, text)
+ Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
+ end
+
+ def trigger
+ params[:command]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_new.rb
index cefb6775db8..016054ecd46 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_new.rb
@@ -1,8 +1,8 @@
module Gitlab
module ChatCommands
- class IssueCreate < IssueCommand
+ class IssueNew < IssueCommand
def self.match(text)
- # we can not match \n with the dot by passing the m modifier as than
+ # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
@@ -19,8 +19,24 @@ module Gitlab
title = match[:title]
description = match[:description].to_s.rstrip
+ issue = create_issue(title: title, description: description)
+
+ if issue.persisted?
+ presenter(issue).present
+ else
+ presenter(issue).display_errors
+ end
+ end
+
+ private
+
+ def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute
end
+
+ def presenter(issue)
+ Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
index 51bf80c800b..3491b53093e 100644
--- a/lib/gitlab/chat_commands/issue_search.rb
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- collection.search(match[:query]).limit(QUERY_LIMIT)
+ issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+ if issues.present?
+ Presenters::IssueSearch.new(issues).present
+ else
+ Presenters::Access.new(issues).not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
index 2a45d49cf6b..d6013f4d10c 100644
--- a/lib/gitlab/chat_commands/issue_show.rb
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- find_by_iid(match[:iid])
+ issue = find_by_iid(match[:iid])
+
+ if issue
+ Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
+ else
+ Gitlab::ChatCommands::Presenters::Access.new.not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
deleted file mode 100644
index 8930a21f406..00000000000
--- a/lib/gitlab/chat_commands/presenter.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module Gitlab
- module ChatCommands
- class Presenter
- include Gitlab::Routing
-
- def authorize_chat_name(url)
- message = if url
- ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
- else
- ":sweat_smile: Couldn't identify you, nor can I autorize you!"
- end
-
- ephemeral_response(message)
- end
-
- def help(commands, trigger)
- if commands.none?
- ephemeral_response("No commands configured")
- else
- commands.map! { |command| "#{trigger} #{command}" }
- message = header_with_list("Available commands", commands)
-
- ephemeral_response(message)
- end
- end
-
- def present(subject)
- return not_found unless subject
-
- if subject.is_a?(Gitlab::ChatCommands::Result)
- show_result(subject)
- elsif subject.respond_to?(:count)
- if subject.none?
- not_found
- elsif subject.one?
- single_resource(subject.first)
- else
- multiple_resources(subject)
- end
- else
- single_resource(subject)
- end
- end
-
- def access_denied
- ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
- end
-
- private
-
- def show_result(result)
- case result.type
- when :success
- in_channel_response(result.message)
- else
- ephemeral_response(result.message)
- end
- end
-
- def not_found
- ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
- end
-
- def single_resource(resource)
- return error(resource) if resource.errors.any? || !resource.persisted?
-
- message = "#{title(resource)}:"
- message << "\n\n#{resource.description}" if resource.try(:description)
-
- in_channel_response(message)
- end
-
- def multiple_resources(resources)
- titles = resources.map { |resource| title(resource) }
-
- message = header_with_list("Multiple results were found:", titles)
-
- ephemeral_response(message)
- end
-
- def error(resource)
- message = header_with_list("The action was not successful, because:", resource.errors.messages)
-
- ephemeral_response(message)
- end
-
- def title(resource)
- reference = resource.try(:to_reference) || resource.try(:id)
- title = resource.try(:title) || resource.try(:name)
-
- "[#{reference} #{title}](#{url(resource)})"
- end
-
- def header_with_list(header, items)
- message = [header]
-
- items.each do |item|
- message << "- #{item}"
- end
-
- message.join("\n")
- end
-
- def url(resource)
- url_for(
- [
- resource.project.namespace.becomes(Namespace),
- resource.project,
- resource
- ]
- )
- end
-
- def ephemeral_response(message)
- {
- response_type: :ephemeral,
- text: message,
- status: 200
- }
- end
-
- def in_channel_response(message)
- {
- response_type: :in_channel,
- text: message,
- status: 200
- }
- end
- end
- end
-end
diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb
new file mode 100644
index 00000000000..92f4fa17f78
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Access < Presenters::Base
+ def access_denied
+ ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+ end
+
+ def not_found
+ ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
+ end
+
+ def authorize
+ message =
+ if @resource
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(text: message)
+ end
+
+ def unknown_command(commands)
+ ephemeral_response(text: help_message(trigger))
+ end
+
+ private
+
+ def help_message(trigger)
+ header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
new file mode 100644
index 00000000000..2700a5a2ad5
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Base
+ include Gitlab::Routing.url_helpers
+
+ def initialize(resource = nil)
+ @resource = resource
+ end
+
+ def display_errors
+ message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+
+ ephemeral_response(text: message)
+ end
+
+ private
+
+ def header_with_list(header, items)
+ message = [header]
+
+ items.each do |item|
+ message << "- #{item}"
+ end
+
+ message.join("\n")
+ end
+
+ def ephemeral_response(message)
+ response = {
+ response_type: :ephemeral,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def in_channel_response(message)
+ response = {
+ response_type: :in_channel,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def format_response(response)
+ response[:text] = format(response[:text]) if response.has_key?(:text)
+
+ if response.has_key?(:attachments)
+ response[:attachments].each do |attachment|
+ attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
+ attachment[:text] = format(attachment[:text]) if attachment[:text]
+ end
+ end
+
+ response
+ end
+
+ # Convert Markdown to slacks format
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def resource_url
+ url_for(
+ [
+ @resource.project.namespace.becomes(Namespace),
+ @resource.project,
+ @resource
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb
new file mode 100644
index 00000000000..863d0bf99ca
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Deploy < Presenters::Base
+ def present(from, to)
+ message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
+
+ in_channel_response(text: message)
+ end
+
+ def no_actions
+ ephemeral_response(text: "No action found to be executed")
+ end
+
+ def too_many_actions
+ ephemeral_response(text: "Too many actions defined")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb
new file mode 100644
index 00000000000..cd47b7f4c6a
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Help < Presenters::Base
+ def present(trigger, text)
+ ephemeral_response(text: help_message(trigger, text))
+ end
+
+ private
+
+ def help_message(trigger, text)
+ return "No commands available :thinking_face:" unless @resource.present?
+
+ if text.start_with?('help')
+ header_with_list("Available commands", full_commands(trigger))
+ else
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ end
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb
new file mode 100644
index 00000000000..dfb1c8f6616
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issuable.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ module Issuable
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignee ? @resource.assignee.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
new file mode 100644
index 00000000000..a1a3add56c9
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueNew < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ in_channel_response(new_issue)
+ end
+
+ private
+
+ def new_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+ end
+
+ def project_link
+ "[#{project.name_with_namespace}](#{projects_url(project)})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
new file mode 100644
index 00000000000..3478359b91d
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueSearch < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ text = if @resource.count >= 5
+ "Here are the first 5 issues I found:"
+ elsif @resource.one?
+ "Here is the only issue I found:"
+ else
+ "Here are the #{@resource.count} issues I found:"
+ end
+
+ ephemeral_response(text: text, attachments: attachments)
+ end
+
+ private
+
+ def attachments
+ @resource.map do |issue|
+ url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+ {
+ color: color(issue),
+ fallback: "#{issue.to_reference} #{issue.title}",
+ text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+ mrkdwn_in: [
+ :text
+ ]
+ }
+ end
+ end
+
+ def project
+ @project ||= @resource.first.project
+ end
+
+ def namespace
+ @namespace ||= project.namespace.becomes(Namespace)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
new file mode 100644
index 00000000000..fe5847ccd15
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueShow < Presenters::Base
+ include Presenters::Issuable
+
+ def present
+ if @resource.confidential?
+ ephemeral_response(show_issue)
+ else
+ in_channel_response(show_issue)
+ end
+ end
+
+ private
+
+ def show_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ text: text,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def text
+ message = "**#{status_text(@resource)}**"
+
+ if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ return message
+ end
+
+ message << " · "
+ message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+ message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+ message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+ message
+ end
+
+ def pretext
+ "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
new file mode 100644
index 00000000000..12a063059cb
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents Coverage settings.
+ #
+ class Coverage < Node
+ include Validatable
+
+ validations do
+ validates :config, regexp: true
+ end
+
+ def value
+ @config[1...-1]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index a4ec8f0ff2f..ede97cc0504 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -33,8 +33,11 @@ module Gitlab
entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.'
+ entry :coverage, Entry::Coverage,
+ description: 'Coverage configuration for this pipeline.'
+
helpers :before_script, :image, :services, :after_script,
- :variables, :stages, :types, :cache, :jobs
+ :variables, :stages, :types, :cache, :coverage, :jobs
def compose!(_deps = nil)
super(self) do
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index a55362f0b6b..69a5e6f433d 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
- after_script variables environment]
+ after_script variables environment coverage]
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
+ entry :coverage, Entry::Coverage,
+ description: 'Coverage configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment
+ :artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies
@@ -130,6 +133,7 @@ module Gitlab
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
+ coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
after_script: after_script_value }
end
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index f01975aab5c..9b9a0a8125a 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
def validate_string_or_regexp(value)
return true if value.is_a?(Symbol)
return false unless value.is_a?(String)
if value.first == '/' && value.last == '/'
- Regexp.new(value[1...-1])
+ validate_regexp(value[1...-1])
else
true
end
- rescue RegexpError
- false
end
def validate_boolean(value)
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index 28b0a9ffe01..16b234e6c59 100644
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -9,15 +9,7 @@ module Gitlab
include Validatable
validations do
- include LegacyValidationHelpers
-
- validate :array_of_strings_or_regexps
-
- def array_of_strings_or_regexps
- unless validate_array_of_strings_or_regexps(config)
- errors.add(:config, 'should be an array of strings or regexps')
- end
- end
+ validates :config, array_of_strings_or_regexps: true
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 8632dd0e233..bd7428b1272 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -54,6 +54,51 @@ module Gitlab
end
end
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+ true
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index a979fe7d573..67bbc3c4849 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'ban'
+ 'icon_action_cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 1bf949c96dd..0f4b7b24cef 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -26,17 +26,13 @@ module Gitlab
end
def action_icon
- 'play'
+ 'icon_action_play'
end
def action_title
'Play'
end
- def action_class
- 'ci-play-icon'
- end
-
def action_path
play_namespace_project_build_path(subject.project.namespace,
subject.project,
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8e38d6a8523..6b362af7634 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'refresh'
+ 'icon_action_retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index e1dfdb76d41..90401cad0d2 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -26,7 +26,7 @@ module Gitlab
end
def action_icon
- 'stop'
+ 'icon_action_stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 73b6ab5a635..3dd2b9e01f6 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -42,9 +42,6 @@ module Gitlab
raise NotImplementedError
end
- def action_class
- end
-
def action_path
raise NotImplementedError
end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 2405b94db50..a09577ae48d 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -41,6 +41,8 @@ module Gitlab
end
def ensure_default_member!
+ @project.project_members.destroy_all
+
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index a2eca74a3c8..036a9307ab5 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -63,7 +63,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
- ProjectCacheWorker.perform(project.id)
+ ProjectCacheWorker.perform_async(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
new file mode 100644
index 00000000000..4a63c760744
--- /dev/null
+++ b/rubocop/cop/gem_fetcher.rb
@@ -0,0 +1,28 @@
+module RuboCop
+ module Cop
+ # Cop that checks for all gems specified in the Gemfile, and will
+ # alert if any gem is to be fetched not from the RubyGems index.
+ # This enforcement is done so as to minimize external build
+ # dependencies and build times.
+ class GemFetcher < RuboCop::Cop::Cop
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+
+ GIT_KEYS = [:git, :github]
+
+ def on_send(node)
+ file_path = node.location.expression.source_buffer.name
+ return unless file_path.end_with?("Gemfile")
+
+ func_name = node.children[1]
+ return unless func_name == :gem
+
+ node.children.last.each_node(:pair) do |pair|
+ key_name = pair.children[0].children[0].to_sym
+ if GIT_KEYS.include?(key_name)
+ add_offense(node, :selector)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7922e19768b..7f20754ee51 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,3 +1,4 @@
require_relative 'migration_helpers'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/column_with_default'
+require_relative 'cop/gem_fetcher'
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index ec6cea5c0f4..3e0326dd47d 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -112,4 +112,49 @@ describe Projects::LabelsController do
post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
end
end
+
+ describe 'POST #promote' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:label_1) { create(:label, title: promoted_label_name, project: project) }
+
+ context 'not group owner' do
+ it 'denies access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'group owner' do
+ before do
+ GroupMember.add_users_to_group(group, [user], :owner)
+ end
+
+ it 'gives access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+
+ it 'promotes the label' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(Label.where(id: label_1.id)).to be_empty
+ expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil
+ end
+
+ context 'service raising InvalidRecord' do
+ before do
+ expect_any_instance_of(Labels::PromoteService).to receive(:execute) do |label|
+ raise ActiveRecord::RecordInvalid.new(label_1)
+ end
+ end
+
+ it 'returns to label list' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 2ae635a1244..cae733f0cfb 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::MattermostsController do
before do
allow_any_instance_of(MattermostSlashCommandsService).
to receive(:list_teams).and_return([])
+ end
+ it 'accepts the request' do
get(:new,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
- end
- it 'accepts the request' do
expect(response).to have_http_status(200)
end
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 32b0e42c3cd..19e948d8fb8 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -6,8 +6,8 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) }
before do
- project.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
end
describe 'GET #index' do
@@ -69,6 +69,86 @@ describe Projects::SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(project, snippet_params = {})
+ sign_in(user)
+
+ project.add_developer(user)
+
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ project.add_master(admin)
+ sign_in(admin)
+
+ post :mark_as_spam,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w[show raw].each do |action|
describe "GET ##{action}" do
context 'when the project snippet is private' do
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index d76fe9f580f..dadcb90cfc2 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -138,6 +138,65 @@ describe SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(snippet_params = {})
+ sign_in(user)
+
+ post :create, {
+ personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:personal_snippet, :public, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ sign_in(admin)
+
+ post :mark_as_spam, id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w(raw download).each do |action|
describe "GET #{action}" do
context 'when the personal snippet is private' do
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
new file mode 100644
index 00000000000..d9be4e5dbdd
--- /dev/null
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard shortcuts', feature: true, js: true do
+ before do
+ login_as :user
+ visit dashboard_projects_path
+ end
+
+ scenario 'Navigate to tabs' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('p')
+
+ ensure_active_main_tab('Projects')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('i')
+
+ ensure_active_main_tab('Issues')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('m')
+
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ def ensure_active_main_tab(content)
+ expect(find('.nav-sidebar li.active')).to have_content(content)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index 56f6cd2e095..511c95b758f 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -19,6 +19,10 @@ feature 'Environment', :feature do
visit_environment(environment)
end
+ scenario 'shows environment name' do
+ expect(page).to have_content(environment.name)
+ end
+
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 72b984cfab8..c033b693213 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do
end
scenario 'does create a new pipeline' do
- expect(page).to have_content('Production')
+ expect(page).to have_content('production')
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 31156fcf994..93139dc9e94 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user, username: 'someone.special') }
+ let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
@@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(find('#at-view-64')).to have_selector('.cur:first-of-type')
end
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys("@#{user.name[0...8]}")
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-64')).to have_content(user.name)
+ end
+
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys('')
diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb
new file mode 100644
index 00000000000..fc8515cfe9b
--- /dev/null
+++ b/spec/features/issues/group_label_sidebar_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe 'Group label on issue', :feature do
+ it 'renders link to the project issues page' do
+ group = create(:group)
+ project = create(:empty_project, :public, namespace: group)
+ feature = create(:group_label, group: group, title: 'feature')
+ issue = create(:labeled_issue, project: project, labels: [feature])
+ label_link = namespace_project_issues_path(
+ project.namespace,
+ project,
+ label_name: [feature.name]
+ )
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ link = find('.issuable-show-labels a')
+
+ expect(link[:href]).to eq(label_link)
+ end
+end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index a4d3053d10c..c0ab42c6822 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Start new branch from an issue', feature: true do
+feature 'Start new branch from an issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -11,7 +11,7 @@ feature 'Start new branch from an issue', feature: true do
login_as(user)
end
- it 'shows the new branch button', js: true do
+ it 'shows the new branch button' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).to have_css('#new-branch .available')
@@ -34,16 +34,26 @@ feature 'Start new branch from an issue', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it "hides the new branch button", js: true do
+ it "hides the new branch button" do
expect(page).to have_css('#new-branch .unavailable')
expect(page).not_to have_css('#new-branch .available')
expect(page).to have_content /1 Related Merge Request/
end
end
+
+ context 'when issue is confidential' do
+ it 'hides the new branch button' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).not_to have_css('#new-branch')
+ end
+ end
end
- context "for visiters" do
- it 'shows no buttons', js: true do
+ context 'for visitors' do
+ it 'shows no buttons' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
new file mode 100644
index 00000000000..44a9b545ff8
--- /dev/null
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'toggler_behavior', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:fragment_id) { "#note_#{note.id}" }
+
+ before do
+ login_as :admin
+ project = merge_request.source_project
+ visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"
+ page.current_window.resize_to(1000, 300)
+ end
+
+ describe 'scroll position' do
+ it 'should be scrolled down to fragment' do
+ page_height = page.current_window.size[1]
+ page_scroll_y = page.evaluate_script("window.scrollY")
+ fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top")
+ expect(find('.js-toggle-content').visible?).to eq true
+ expect(find(fragment_id).visible?).to eq true
+ expect(fragment_position_top).to be >= page_scroll_y
+ expect(fragment_position_top).to be < (page_scroll_y + page_height)
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index b13674b4db9..2582a540240 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
end
describe 'merge-request-only commands' do
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index b785b2f7704..fab2d532e06 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -89,7 +89,7 @@ describe 'Comments', feature: true do
end
end
- it 'should reset the edit note form textarea with the original content of the note if cancelled' do
+ it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
@@ -198,7 +198,7 @@ describe 'Comments', feature: true do
end
describe 'the note form' do
- it "shouldn't add a second form for same row" do
+ it "does not add a second form for same row" do
click_diff_line
is_expected.
@@ -206,7 +206,7 @@ describe 'Comments', feature: true do
count: 1)
end
- it 'should be removed when canceled' do
+ it 'is removed when canceled' do
is_expected.to have_css('.js-temp-notes-holder')
page.within("form[data-line-code='#{line_code}']") do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index abfc46601fb..b56e562b2b6 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,11 +1,13 @@
require "spec_helper"
feature "New project", feature: true do
- context "Visibility level selector" do
- let(:user) { create(:admin) }
+ let(:user) { create(:admin) }
- before { login_as(user) }
+ before do
+ login_as(user)
+ end
+ context "Visibility level selector" do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
@@ -16,4 +18,16 @@ feature "New project", feature: true do
end
end
end
+
+ context 'Import project options' do
+ before do
+ visit new_project_path
+ end
+
+ it 'does not autocomplete sensitive git repo URL' do
+ autocomplete = find('#project_import_url')['autocomplete']
+
+ expect(autocomplete).to eq('off')
+ end
+ end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index e673ece37c3..917b545e98b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -66,8 +66,8 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('.ci-status-icon-running')
- expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+ expect(page).to have_selector('.js-ci-status-icon-running')
+ expect(page).to have_selector('.js-icon-action-cancel')
expect(page).to have_content('deploy')
end
end
@@ -82,12 +82,12 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('build')
end
page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
@@ -101,12 +101,12 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has failed builds' do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_selector('.js-ci-status-icon-failed')
expect(page).to have_content('test')
end
page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
@@ -120,12 +120,12 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has manual builds' do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('.ci-status-icon-manual')
+ expect(page).to have_selector('.js-ci-status-icon-manual')
expect(page).to have_content('manual')
end
page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-play')
+ expect(page).to have_selector('.js-icon-action-play')
end
end
@@ -138,7 +138,7 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has external build' do
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('jenkins')
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 86a07b2c679..042a1ccab51 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -99,6 +99,15 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(select_element.all('option').count).to eq(3)
end
+ it 'shows an error alert with the error message if there is an error requesting teams' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_selector('.alert')
+ expect(page).to have_content('test mattermost error message')
+ end
+
def stub_teams(count: 0)
teams = create_teams(count)
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 468bcc7badc..eae097126ce 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -134,7 +134,7 @@ describe DiffHelper do
let(:new_pos) { 50 }
let(:text) { 'some_text' }
- it "should generate foldable top match line for inline view with empty text by default" do
+ it "generates foldable top match line for inline view with empty text by default" do
output = diff_match_line old_pos, new_pos
expect(output).to be_html_safe
@@ -143,7 +143,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
end
- it "should allow to define text and bottom option" do
+ it "allows to define text and bottom option" do
output = diff_match_line old_pos, new_pos, text: text, bottom: true
expect(output).to be_html_safe
@@ -152,7 +152,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
end
- it "should generate match line for parallel view" do
+ it "generates match line for parallel view" do
output = diff_match_line old_pos, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
@@ -162,7 +162,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
end
- it "should allow to generate only left match line for parallel view" do
+ it "allows to generate only left match line for parallel view" do
output = diff_match_line old_pos, nil, text: text, view: :parallel
expect(output).to be_html_safe
@@ -171,7 +171,7 @@ describe DiffHelper do
expect(output).not_to have_css 'td:nth-child(3)'
end
- it "should allow to generate only right match line for parallel view" do
+ it "allows to generate only right match line for parallel view" do
output = diff_match_line nil, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 71446b9df61..f1bfd529983 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -113,7 +113,7 @@
});
});
describe('::getAwardUrl', function() {
- return it('should return the url for request', function() {
+ return it('returns the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
new file mode 100644
index 00000000000..5eba4343a1d
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
@@ -0,0 +1,40 @@
+//= require filtered_search/dropdown_utils
+//= require filtered_search/filtered_search_tokenizer
+//= require filtered_search/filtered_search_dropdown
+//= require filtered_search/dropdown_user
+
+(() => {
+ describe('Dropdown User', () => {
+ describe('getSearchInput', () => {
+ let dropdownUser;
+
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
+
+ dropdownUser = new gl.DropdownUser();
+ });
+
+ it('should not return the double quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '"johnny appleseed',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ });
+
+ it('should not return the single quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '\'larry boy',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('larry boy');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
index c8b5c2b36ad..a508dacf7f0 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
@@ -23,6 +23,7 @@
`);
spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index bf45100af03..6f1d6406897 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
/*= require merge_request_widget */
+/*= require smart_interval */
/*= require lib/utils/datetime_utility */
(function() {
@@ -21,7 +22,11 @@
normal: "Build {{status}}"
},
gitlab_icon: "gitlab_logo.png",
- builds_path: "http://sampledomain.local/sampleBuildsPath"
+ ci_pipeline: 80,
+ ci_sha: "12a34bc5",
+ builds_path: "http://sampledomain.local/sampleBuildsPath",
+ commits_path: "http://sampledomain.local/commits",
+ pipeline_path: "http://sampledomain.local/pipelines"
};
this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
@@ -118,10 +123,11 @@
});
});
- return describe('getCIStatus', function() {
+ describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
"title": "Sample MR title",
+ "pipeline": 80,
"sha": "12a34bc5",
"status": "success",
"coverage": 98
@@ -165,6 +171,22 @@
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
+ it('should update the pipeline URL when the pipeline changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.pipeline += 1;
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
+ it('should update the commit URL when the sha changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.sha = "9b50b99a";
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
});
});
}).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 386fc8f514e..db11c2516a6 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -11,9 +11,9 @@
beforeEach(function() {
loadFixtures(fixtureName);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
- return this.shortcut = new ShortcutsIssuable();
+ this.shortcut = new ShortcutsIssuable();
});
- return describe('#replyWithSelectedText', function() {
+ describe('#replyWithSelectedText', function() {
var stubSelection;
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
stubSelection = function(html) {
@@ -24,56 +24,61 @@
};
};
beforeEach(function() {
- return this.selector = 'form.js-main-target-form textarea#note_note';
+ this.selector = 'form.js-main-target-form textarea#note_note';
});
describe('with empty selection', function() {
- return it('does nothing', function() {
- stubSelection('');
+ it('does not return an error', function() {
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe('');
+ expect($(this.selector).val()).toBe('');
+ });
+ it('triggers `input`', function() {
+ var focused = false;
+ $(this.selector).on('focus', function() {
+ focused = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ expect(focused).toBe(true);
});
});
describe('with any selection', function() {
beforeEach(function() {
- return stubSelection('<p>Selected text.</p>');
+ stubSelection('<p>Selected text.</p>');
});
it('leaves existing input intact', function() {
$(this.selector).val('This text was already here.');
expect($(this.selector).val()).toBe('This text was already here.');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
+ expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
});
it('triggers `input`', function() {
- var triggered;
- triggered = false;
+ var triggered = false;
$(this.selector).on('input', function() {
- return triggered = true;
+ triggered = true;
});
this.shortcut.replyWithSelectedText();
- return expect(triggered).toBe(true);
+ expect(triggered).toBe(true);
});
- return it('triggers `focus`', function() {
- var focused;
- focused = false;
+ it('triggers `focus`', function() {
+ var focused = false;
$(this.selector).on('focus', function() {
- return focused = true;
+ focused = true;
});
this.shortcut.replyWithSelectedText();
- return expect(focused).toBe(true);
+ expect(focused).toBe(true);
});
});
describe('with a one-line selection', function() {
- return it('quotes the selection', function() {
+ it('quotes the selection', function() {
stubSelection('<p>This text has been selected.</p>');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
+ expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
});
});
- return describe('with a multi-line selection', function() {
- return it('quotes the selected lines as a group', function() {
+ describe('with a multi-line selection', function() {
+ it('quotes the selected lines as a group', function() {
stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
+ expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
});
});
});
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index f824e2e1efe..49349035b3b 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -4,6 +4,33 @@ module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
+ describe '#build_attributes' do
+ context 'Coverage entry' do
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+
+ let(:config_base) { { rspec: { script: "rspec" } } }
+ let(:config) { YAML.dump(config_base) }
+
+ context 'when config has coverage set at the global scope' do
+ before do
+ config_base.update(coverage: '/\(\d+\.\d+\) covered/')
+ end
+
+ context "and 'rspec' job doesn't have coverage set" do
+ it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') }
+ end
+
+ context "but 'rspec' job also has coverage set" do
+ before do
+ config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/'
+ end
+
+ it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') }
+ end
+ end
+ end
+ end
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -21,6 +48,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {},
allow_failure: false,
@@ -435,6 +463,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -463,6 +492,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.5",
@@ -702,6 +732,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -913,6 +944,7 @@ module Ci
stage_idx: 1,
name: "normal_job",
commands: "test",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -958,6 +990,7 @@ module Ci
stage_idx: 0,
name: "job1",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -970,6 +1003,7 @@ module Ci
stage_idx: 0,
name: "job2",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f251c0dd25a..b234de4c772 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
- it 'recognizes user lfs tokens' do
- user = create(:user)
- token = Gitlab::LfsToken.new(user).token
+ context 'while using LFS authenticate' do
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ token = Gitlab::LfsToken.new(user).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ end
- it 'recognizes deploy key lfs tokens' do
- key = create(:deploy_key)
- token = Gitlab::LfsToken.new(key).token
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ token = Gitlab::LfsToken.new(key).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ end
- context "while using OAuth tokens as passwords" do
- it 'succeeds for OAuth tokens with the `api` scope' do
+ it 'does not try password auth before oauth' do
user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
+ token = Gitlab::LfsToken.new(user).token
+
+ expect(gl_auth).not_to receive(:find_with_user_password)
+ gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using OAuth tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+
+ it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
- user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before oauth' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')
+ end
end
- context "while using personal access tokens as passwords" do
- it 'succeeds for personal access tokens with the `api` scope' do
- user = create(:user)
- personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
+ context 'while using personal access tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
+ it 'succeeds for personal access tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
end
it 'fails for personal access tokens with other scopes' do
- user = create(:user)
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before personal access tokens' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using regular user and password' do
+ it 'falls through lfs authentication' do
+ user = create(
+ :user,
+ username: 'normal_user',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+
+ it 'falls through oauth authentication when the username is oauth2' do
+ user = create(
+ :user,
+ username: 'oauth2',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
end
it 'returns double nil for invalid credentials' do
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index 1e81eaef18c..b6e924d67be 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ChatCommands::Command, service: true do
it 'displays the help message' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Available commands')
+ expect(subject[:text]).to start_with('Unknown command')
expect(subject[:text]).to match('/gitlab issue show')
end
end
@@ -34,47 +34,7 @@ describe Gitlab::ChatCommands::Command, service: true do
it 'rejects the actions' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Whoops! That action is not allowed')
- end
- end
-
- context 'issue is successfully created' do
- let(:params) { { text: "issue create my new issue" } }
-
- before do
- project.team << [user, :master]
- end
-
- it 'presents the issue' do
- expect(subject[:text]).to match("my new issue")
- end
-
- it 'shows a link to the new issue' do
- expect(subject[:text]).to match(/\/issues\/\d+/)
- end
- end
-
- context 'searching for an issue' do
- let(:params) { { text: 'issue search find me' } }
- let!(:issue) { create(:issue, project: project, title: 'find me') }
-
- before do
- project.team << [user, :master]
- end
-
- context 'a single issue is found' do
- it 'presents the issue' do
- expect(subject[:text]).to match(issue.title)
- end
- end
-
- context 'multiple issues found' do
- let!(:issue2) { create(:issue, project: project, title: "someone find me") }
-
- it 'shows a link to the new issue' do
- expect(subject[:text]).to match(issue.title)
- expect(subject[:text]).to match(issue2.title)
- end
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -90,7 +50,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -100,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end
it 'returns action' do
- expect(subject[:text]).to include('Deployment from staging to production started.')
+ expect(subject[:text]).to include('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel)
end
@@ -130,7 +90,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } }
- it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) }
end
context 'IssueSearch is triggered' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index bd8099c92da..b3358a32161 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
context 'if no environment is defined' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
context 'when duplicate action exists' do
@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns error' do
- expect(subject.type).to eq(:error)
- expect(subject.message).to include('Too many actions defined')
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq('Too many actions defined')
end
end
@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
name: 'teardown', environment: 'production')
end
- it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ it 'returns the success message' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
end
end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
index 6c71e79ff6d..84c22328064 100644
--- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::ChatCommands::IssueCreate, service: true do
+describe Gitlab::ChatCommands::IssueNew, service: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
it 'creates the issue' do
expect { subject }.to change { project.issues.count }.by(1)
- expect(subject.title).to eq('bird is the word')
+ expect(subject[:response_type]).to be(:in_channel)
end
end
@@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
expect { subject }.to change { project.issues.count }.by(1)
end
end
+
+ context 'issue cannot be created' do
+ let!(:issue) { create(:issue, project: project, title: 'bird is the word') }
+ let(:regex_match) { described_class.match("issue create #{'a' * 512}}") }
+
+ it 'displays the errors' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("- Title is too long")
+ end
+ end
end
describe '.match' do
diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
index 24c06a967fa..551ccb79a58 100644
--- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueSearch, service: true do
describe '#execute' do
- let!(:issue) { create(:issue, title: 'find me') }
+ let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
- let(:project) { issue.project }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue search find") }
@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
context 'when the user has no access' do
it 'only returns the open issues' do
- expect(subject).not_to include(confidential)
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
end
it 'returns all results' do
- expect(subject).to include(confidential, issue)
+ expect(subject).to have_key(:attachments)
+ expect(subject[:text]).to eq("Here are the 2 issues I found:")
end
end
context 'without hits on the query' do
it 'returns an empty collection' do
- expect(subject).to be_empty
+ expect(subject[:text]).to match("not found")
end
end
end
diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
index 2eab73e49e5..1f20d0a44ce 100644
--- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueShow, service: true do
describe '#execute' do
- let(:issue) { create(:issue) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, project: project) }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
end
context 'the issue exists' do
+ let(:title) { subject[:attachments].first[:title] }
+
it 'returns the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
context 'when its reference is given' do
let(:regex_match) { described_class.match("issue show #{issue.to_reference}") }
it 'shows the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
end
end
@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
context 'the issue does not exist' do
let(:regex_match) { described_class.match("issue show 2343242") }
- it "returns nil" do
- expect(subject).to be_nil
+ it "returns not found" do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
end
- describe 'self.match' do
+ describe '.match' do
it 'matches the iid' do
match = described_class.match("issue show 123")
expect(match[:iid]).to eq("123")
end
+
+ it 'accepts a reference' do
+ match = described_class.match("issue show #{Issue.reference_prefix}123")
+
+ expect(match[:iid]).to eq("123")
+ end
end
end
diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
new file mode 100644
index 00000000000..ae41d75ab0c
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Access do
+ describe '#access_denied' do
+ subject { described_class.new.access_denied }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'displays an error message' do
+ expect(subject[:text]).to match("is not allowed")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#not_found' do
+ subject { described_class.new.not_found }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user the resource was not found' do
+ expect(subject[:text]).to match("not found!")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#authorize' do
+ context 'with an authorization URL' do
+ subject { described_class.new('http://authorize.me').authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("connect your GitLab account")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ context 'without authorization url' do
+ subject { described_class.new.authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("Couldn't identify you")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
new file mode 100644
index 00000000000..dc2dd300072
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Deploy do
+ let(:build) { create(:ci_build) }
+
+ describe '#present' do
+ subject { described_class.new(build).present('staging', 'prod') }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'messages the channel of the deploy' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with("Deployment started from staging to prod")
+ end
+ end
+
+ describe '#no_actions' do
+ subject { described_class.new(nil).no_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
+ end
+ end
+
+ describe '#too_many_actions' do
+ subject { described_class.new([]).too_many_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("Too many actions defined")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
new file mode 100644
index 00000000000..17fcdbc2452
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueNew do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
new file mode 100644
index 00000000000..ec6d3e34a96
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueSearch do
+ let(:project) { create(:empty_project) }
+ let(:message) { subject[:text] }
+
+ before { create_list(:issue, 2, project: project) }
+
+ subject { described_class.new(project.issues).present }
+
+ it 'formats the message correct' do
+ is_expected.to have_key(:text)
+ is_expected.to have_key(:status)
+ is_expected.to have_key(:response_type)
+ is_expected.to have_key(:attachments)
+ end
+
+ it 'shows a list of results' do
+ expect(subject[:response_type]).to be(:ephemeral)
+
+ expect(message).to start_with("Here are the 2 issues I found")
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
new file mode 100644
index 00000000000..5b678d31fce
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueShow do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+
+ context 'with upvotes' do
+ before do
+ create(:award_emoji, :upvote, awardable: issue)
+ end
+
+ it 'shows the upvote count' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open** · :+1: 1")
+ end
+ end
+
+ context 'confidential issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'shows an ephemeral response' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open**")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
new file mode 100644
index 00000000000..4c6bd859552
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Coverage do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context "when entry config value doesn't have the surrounding '/'" do
+ let(:config) { 'Code coverage: \d+\.\d+' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ context "when entry config value has the surrounding '/'" do
+ let(:config) { '/Code coverage: \d+\.\d+/' }
+
+ describe '#value' do
+ subject { entry.value }
+ it { is_expected.to eq(config[1...-1]) }
+ end
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to be_empty }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { '(malformed regexp' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index e64c8d46bd8..d4f1780b174 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -4,12 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do
let(:global) { described_class.new(hash) }
describe '.nodes' do
- it 'can contain global config keys' do
- expect(described_class.nodes).to include :before_script
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a(Hash)
end
- it 'returns a hash' do
- expect(described_class.nodes).to be_a Hash
+ context 'when filtering all the entry/node names' do
+ it 'contains the expected node names' do
+ node_names = described_class.nodes.keys
+ expect(node_names).to match_array(%i[before_script image services
+ after_script variables stages
+ types cache coverage])
+ end
end
end
@@ -35,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do
end
it 'creates node object for each entry' do
- expect(global.descendants.count).to eq 8
+ expect(global.descendants.count).to eq 9
end
it 'creates node object using valid class' do
@@ -176,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#nodes' do
it 'instantizes all nodes' do
- expect(global.descendants.count).to eq 8
+ expect(global.descendants.count).to eq 9
end
it 'contains unspecified nodes' do
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index fc9b8b86dc4..d20f4ec207d 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -3,6 +3,20 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
+ describe '.nodes' do
+ context 'when filtering all the entry/node names' do
+ subject { described_class.nodes.keys }
+
+ let(:result) do
+ %i[before_script script stage type after_script cache
+ image services only except variables artifacts
+ environment coverage]
+ end
+
+ it { is_expected.to match_array result }
+ end
+ end
+
describe 'validations' do
before { entry.compose! }
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index b3c07347de1..8ad9b7cdf07 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'ban' }
+ it { expect(subject.action_icon).to eq 'icon_action_cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f1b50a59ce6..f3e72ea1796 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'play' }
+ it { expect(subject.action_icon).to eq 'icon_action_play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 62036f8ec5d..2db0f8d29bd 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'refresh' }
+ it { expect(subject.action_icon).to eq 'icon_action_retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 597e02e86e4..41c2b624774 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'stop' }
+ it { expect(subject.action_icon).to eq 'icon_action_stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 1e21270d928..5893485634d 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with a diff file" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
@@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with diff lines" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index fe5fa048413..0f779339c54 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
subject { described_class.new(diff_file) }
describe '#parallelize' do
- it 'should return an array of arrays containing the parsed diff' do
+ it 'returns an array of arrays containing the parsed diff' do
diff_lines = diff_file.highlighted_diff_lines
expected = [
# Unchanged lines
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index fadfe4d378e..e177d883158 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do
Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
end
- it 'should properly highlight all the lines' do
+ it 'highlights all the lines properly' do
expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7fb6829f582..20241d4d63e 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -52,6 +52,7 @@ snippets:
- project
- notes
- award_emoji
+- user_agent_detail
releases:
- project
project_members:
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 0b7984d6ca9..f2cb028206f 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -92,5 +92,29 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
end
end
+
+ context 'importer same as group member' do
+ let(:user2) { create(:admin, authorized_projects_populated: true) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user2, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ end
+
+ it 'maps the project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+
+ it 'maps the project member if it already exists' do
+ project.add_master(user2)
+
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 493bc2db21a..95b230e4f5c 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -222,6 +222,7 @@ CommitStatus:
- queued_at
- token
- lock_version
+- coverage_regex
Ci::Variable:
- id
- project_id
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index b9d12c3c24c..9dd997aa7dc 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
- it 'should block user in GitLab' do
+ it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 47cd5075a7d..e20b394c525 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -221,6 +221,47 @@ describe Ci::Build, :models do
end
end
+ describe '#coverage_regex' do
+ subject { build.coverage_regex }
+
+ context 'when project has build_coverage_regex set' do
+ let(:project_regex) { '\(\d+\.\d+\) covered' }
+
+ before do
+ project.build_coverage_regex = project_regex
+ end
+
+ context 'and coverage_regex attribute is not set' do
+ it { is_expected.to eq(project_regex) }
+ end
+
+ context 'but coverage_regex attribute is also set' do
+ let(:build_regex) { 'Code coverage: \d+\.\d+' }
+
+ before do
+ build.coverage_regex = build_regex
+ end
+
+ it { is_expected.to eq(build_regex) }
+ end
+ end
+
+ context 'when neither project nor build has coverage regex set' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#update_coverage' do
+ context "regarding coverage_regex's value," do
+ it "saves the correct extracted coverage value" do
+ build.coverage_regex = '\(\d+.\d+\%\) covered'
+ allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
+ expect(build).to receive(:update_attributes).with(coverage: 98.29) { true }
+ expect(build.update_coverage).to be true
+ end
+ end
+ end
+
describe 'deployment' do
describe '#last_deployment' do
subject { build.last_deployment }
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 3b7cc7d9e2e..9053485939e 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- deploy_master
- end
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
expect(subject[:code].median).to be_nil
end
@@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:code].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 5c73edbbc53..fc7d18bd40e 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
- 5.times do
- regular_label = create(:label)
- issue = create(:issue, project: project)
- issue.update(label_ids: [regular_label.id])
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
- create_merge_request_closing_issue(issue)
- merge_merge_requests_closing_issue(issue)
- end
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
expect(subject[:issue].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 591bbdddf55..2cbee741fb0 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -29,11 +29,9 @@ describe 'CycleAnalytics#production', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
expect(subject[:production].median).to be_nil
end
@@ -41,12 +39,10 @@ describe 'CycleAnalytics#production', feature: true do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
expect(subject[:production].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 33d2c0a7416..febb18c9884 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do
context "when a regular merge request (that doesn't close the issue) is created and merged" do
it "returns nil" do
- 5.times do
- MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
- end
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
expect(subject[:review].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 00693d67475..104e65335dd 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
expect(subject[:staging].median).to be_nil
end
@@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
expect(subject[:staging].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index f857ea6cbec..c2ba012a0e6 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.succeed!
+ pipeline.run!
+ pipeline.succeed!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
@@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is not for a merge request" do
it "returns nil" do
- 5.times do
- pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
- pipeline.run!
- pipeline.succeed!
- end
+ pipeline.run!
+ pipeline.succeed!
expect(subject[:test].median).to be_nil
end
@@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is dropped (failed)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.drop!
+ pipeline.run!
+ pipeline.drop!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
@@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is cancelled" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.cancel!
+ pipeline.run!
+ pipeline.cancel!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index b2e06541e66..eba392044bf 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -288,6 +288,11 @@ describe Environment, models: true do
"1-foo" => "env-1-foo" + SUFFIX,
"1/foo" => "env-1-foo" + SUFFIX,
"foo-" => "foo" + SUFFIX,
+ "foo--bar" => "foo-bar" + SUFFIX,
+ "foo**bar" => "foo-bar" + SUFFIX,
+ "*-foo" => "env-foo" + SUFFIX,
+ "staging-12345678-" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 90d14c2c0b9..e4be0aba7a6 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -117,7 +117,7 @@ describe ProjectMember, models: true do
users = create_list(:user, 2)
described_class.add_users_to_projects(
- [projects.first.id, projects.second],
+ [projects.first.id, projects.second.id],
[users.first.id, users.second],
described_class::MASTER)
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index c879edddfdd..98f3d420c8a 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -113,10 +113,7 @@ describe MattermostSlashCommandsService, :models do
end
it 'shows error messages' do
- teams, message = subject
-
- expect(teams).to be_empty
- expect(message).to eq('Failed to get team list.')
+ expect(subject).to eq([[], "Failed to get team list."])
end
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 645e36683bc..bd6e23ee769 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -67,7 +67,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return project builds' do
+ it 'does not return project builds' do
expect(response).to have_http_status(401)
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1187d2e609d..a027c23bb88 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -326,7 +326,7 @@ describe API::Groups, api: true do
expect(response).to have_http_status(404)
end
- it "should only return projects to which user has access" do
+ it "only returns projects to which user has access" do
project3.team << [user3, :developer]
get api("/groups/#{group1.id}/projects", user3)
@@ -338,7 +338,7 @@ describe API::Groups, api: true do
end
context "when authenticated as admin" do
- it "should return any existing group" do
+ it "returns any existing group" do
get api("/groups/#{group2.id}/projects", admin)
expect(response).to have_http_status(200)
@@ -346,7 +346,7 @@ describe API::Groups, api: true do
expect(json_response.first['name']).to eq(project2.name)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328/projects", admin)
expect(response).to have_http_status(404)
@@ -354,7 +354,7 @@ describe API::Groups, api: true do
end
context 'when using group path in URL' do
- it 'should return any existing group' do
+ it 'returns any existing group' do
get api("/groups/#{group1.path}/projects", admin)
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 01032c0929b..45d5ae267c5 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -4,6 +4,7 @@ describe API::ProjectSnippets, api: true do
include ApiHelpers
let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
let(:admin) { create(:admin) }
describe 'GET /projects/:project_id/snippets/:id' do
@@ -22,7 +23,7 @@ describe API::ProjectSnippets, api: true do
let(:user) { create(:user) }
it 'returns all snippets available to team member' do
- project.team << [user, :developer]
+ project.add_developer(user)
public_snippet = create(:project_snippet, :public, project: project)
internal_snippet = create(:project_snippet, :internal, project: project)
private_snippet = create(:project_snippet, :private, project: project)
@@ -50,7 +51,7 @@ describe API::ProjectSnippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
code: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -72,6 +73,51 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a1db81ce18c..753dde0ca3a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -459,7 +459,7 @@ describe API::Projects, api: true do
before { project }
before { admin }
- it 'should create new project without path and return 201' do
+ it 'creates new project without path and return 201' do
expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index f6fb6ea5506..6b9a739b439 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -80,7 +80,7 @@ describe API::Snippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -101,6 +101,36 @@ describe API::Snippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(snippet_params = {})
+ post api('/snippets', user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'PUT /snippets/:id' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 8dbe5f0b025..1cedaa4ba63 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -458,7 +458,7 @@ describe Ci::API::Builds do
before { build.run! }
describe "POST /builds/:id/artifacts/authorize" do
- context "should authorize posting artifact to running build" do
+ context "authorizes posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
@@ -492,7 +492,7 @@ describe Ci::API::Builds do
end
end
- context "should fail to post too large artifact" do
+ context "fails to post too large artifact" do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index b7dc99ed887..f2c2009bcbf 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -9,7 +9,7 @@ describe EventCreateService, services: true do
it { expect(service.open_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -19,7 +19,7 @@ describe EventCreateService, services: true do
it { expect(service.close_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -29,7 +29,7 @@ describe EventCreateService, services: true do
it { expect(service.reopen_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.reopen_issue(issue, issue.author) }.to change { Event.count }
end
end
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
new file mode 100644
index 00000000000..4b90ad19640
--- /dev/null
+++ b/spec/services/labels/promote_service_spec.rb
@@ -0,0 +1,187 @@
+require 'spec_helper'
+
+describe Labels::PromoteService, services: true do
+ describe '#execute' do
+ let!(:user) { create(:user) }
+
+ context 'project without group' do
+ let!(:project_1) { create(:empty_project) }
+
+ let!(:project_label_1_1) { create(:label, project: project_1) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on project without group' do
+ expect(service.execute(project_label_1_1)).to be_falsey
+ end
+ end
+
+ context 'project with group' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:untouched_label_name) { "Untouched Label" }
+ let!(:promoted_description) { "Promoted Description" }
+ let!(:promoted_color) { "#0000FF" }
+ let!(:label_2_1_priority) { 1 }
+ let!(:label_3_1_priority) { 2 }
+
+ let!(:group_1) { create(:group) }
+ let!(:group_2) { create(:group) }
+
+ let!(:project_1) { create(:empty_project, namespace: group_1) }
+ let!(:project_2) { create(:empty_project, namespace: group_1) }
+ let!(:project_3) { create(:empty_project, namespace: group_1) }
+ let!(:project_4) { create(:empty_project, namespace: group_2) }
+
+ # Labels/issues can't be lazily created so we might as well eager initialize
+ # all other objects too since we use them inside
+ let!(:project_label_1_1) { create(:label, project: project_1, name: promoted_label_name, color: promoted_color, description: promoted_description) }
+ let!(:project_label_1_2) { create(:label, project: project_1, name: untouched_label_name) }
+ let!(:project_label_2_1) { create(:label, project: project_2, priority: label_2_1_priority, name: promoted_label_name, color: "#FF0000") }
+ let!(:project_label_3_1) { create(:label, project: project_3, priority: label_3_1_priority, name: promoted_label_name) }
+ let!(:project_label_3_2) { create(:label, project: project_3, priority: 1, name: untouched_label_name) }
+ let!(:project_label_4_1) { create(:label, project: project_4, name: promoted_label_name) }
+
+ let!(:issue_1_1) { create(:labeled_issue, project: project_1, labels: [project_label_1_1, project_label_1_2]) }
+ let!(:issue_1_2) { create(:labeled_issue, project: project_1, labels: [project_label_1_2]) }
+ let!(:issue_2_1) { create(:labeled_issue, project: project_2, labels: [project_label_2_1]) }
+ let!(:issue_4_1) { create(:labeled_issue, project: project_4, labels: [project_label_4_1]) }
+
+ let!(:merge_3_1) { create(:labeled_merge_request, source_project: project_3, target_project: project_3, labels: [project_label_3_1, project_label_3_2]) }
+
+ let!(:issue_board_2_1) { create(:board, project: project_2) }
+ let!(:issue_board_list_2_1) { create(:list, board: issue_board_2_1, label: project_label_2_1) }
+
+ let(:new_label) { group_1.labels.find_by(title: promoted_label_name) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on group label' do
+ group_label = create(:group_label, group: group_1)
+
+ expect(service.execute(group_label)).to be_falsey
+ end
+
+ it 'is truthy on success' do
+ expect(service.execute(project_label_1_1)).to be_truthy
+ end
+
+ it 'recreates the label as a group label' do
+ expect { service.execute(project_label_1_1) }.
+ to change(project_1.labels, :count).by(-1).
+ and change(group_1.labels, :count).by(1)
+ expect(new_label).not_to be_nil
+ end
+
+ it 'copies title, description and color' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.title).to eq(promoted_label_name)
+ expect(new_label.description).to eq(promoted_description)
+ expect(new_label.color).to eq(promoted_color)
+ end
+
+ it 'merges labels with the same name in group' do
+ expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \
+ change(project_3.labels, :count).by(-1)
+ end
+
+ it 'recreates priorities' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_1)).to be_nil
+ expect(new_label.priority(project_2)).to eq(label_2_1_priority)
+ expect(new_label.priority(project_3)).to eq(label_3_1_priority)
+ end
+
+ it 'does not touch project out of promoted group' do
+ service.execute(project_label_1_1)
+ project_4_new_label = project_4.labels.find_by(title: promoted_label_name)
+
+ expect(project_4_new_label).not_to be_nil
+ expect(project_4_new_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'does not touch out of group priority' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_4)).to be_nil
+ end
+
+ it 'relinks issue with the promoted label' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not remove untouched labels from issue' do
+ expect { service.execute(project_label_1_1) }.not_to change(issue_1_1.labels, :count)
+ end
+
+ it 'does not relink untouched label in issue' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_2.labels.find_by(title: untouched_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_1_2.id)
+ end
+
+ it 'relinks issues with merged labels' do
+ service.execute(project_label_1_1)
+ issue_label = issue_2_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not relink issues from other group' do
+ service.execute(project_label_1_1)
+ issue_label = issue_4_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'updates merge request' do
+ service.execute(project_label_1_1)
+ merge_label = merge_3_1.labels.find_by(title: promoted_label_name)
+
+ expect(merge_label).not_to be_nil
+ expect(merge_label.id).to eq(new_label.id)
+ end
+
+ it 'updates board lists' do
+ service.execute(project_label_1_1)
+ list = issue_board_2_1.lists.find_by(label: new_label)
+
+ expect(list).not_to be_nil
+ end
+
+ # In case someone adds a new relation to Label.rb and forgets to relink it
+ # and the database doesn't have foreign key constraints
+ it 'relinks all relations' do
+ service.execute(project_label_1_1)
+
+ Label.reflect_on_all_associations.each do |association|
+ expect(project_label_1_1.send(association.name).any?).to be_falsey
+ end
+ end
+
+ context 'with invalid group label' do
+ before do
+ allow(service).to receive(:clone_label_to_group_label).and_wrap_original do |m, *args|
+ label = m.call(*args)
+ allow(label).to receive(:valid?).and_return(false)
+
+ label
+ end
+ end
+
+ it 'raises an exception' do
+ expect { service.execute(project_label_1_1) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 5f6a7716beb..d55a7657c0e 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do
it { expect(@merge_request).to be_valid }
it { expect(@merge_request).to be_closed }
- it 'should execute hooks with close action' do
+ it 'executes hooks with close action' do
expect(service).to have_received(:execute_hooks).
with(@merge_request, 'close')
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 309985a5d90..7cf2cd9968f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -443,6 +443,8 @@ describe NotificationService, services: true do
before do
build_team(issue.project)
+ build_group(issue.project)
+
add_users_with_subscription(issue.project, issue)
reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
@@ -459,6 +461,8 @@ describe NotificationService, services: true do
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
+ should_email(@g_global_watcher)
+ should_email(@g_watcher)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -1218,6 +1222,22 @@ describe NotificationService, services: true do
project.add_master(@u_custom_global)
end
+ # Users in the project's group but not part of project's team
+ # with different notification settings
+ def build_group(project)
+ group = create(:group, :public)
+ project.group = group
+
+ # Group member: global=disabled, group=watch
+ @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group)
+ @g_watcher.notification_settings_for(nil).disabled!
+
+ # Group member: global=watch, group=global
+ @g_global_watcher = create_global_setting_for(create(:user), :watch)
+ group.add_users([@g_watcher, @g_global_watcher], :master)
+ group
+ end
+
def create_global_setting_for(user, level)
setting = user.global_notification_setting
setting.level = level
@@ -1226,9 +1246,9 @@ describe NotificationService, services: true do
user
end
- def create_user_with_notification(level, username)
+ def create_user_with_notification(level, username, resource = project)
user = create(:user, username: username)
- setting = user.notification_settings_for(project)
+ setting = user.notification_settings_for(resource)
setting.level = level
setting.save
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 10b90b40ba7..19b32c84d81 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -63,22 +63,20 @@ module CycleAnalyticsHelpers
# test case.
allow(self).to receive(:project) { other_project }
- 5.times do
- data = data_fn[self]
- start_time = Time.now
- end_time = rand(1..10).days.from_now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
- end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
- end
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
end
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
# Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original
@@ -114,17 +112,15 @@ module CycleAnalyticsHelpers
context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- end_time = rand(1..10).days.from_now
-
- end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
- Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
end
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
expect(subject[phase].median).to be_nil
end
end
@@ -133,17 +129,15 @@ module CycleAnalyticsHelpers
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- start_time = Time.now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
- post_fn[self, data] if post_fn
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
end
+ post_fn[self, data] if post_fn
+
expect(subject[phase].median).to be_nil
end
end
diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
index 80fc8c48fed..8d1cff7a261 100644
--- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
+++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
@@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do
Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting"
end
- it 'should run the task without errors' do
+ it 'runs the task without errors' do
expect { run_rake_task }.not_to raise_error
end
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 1b910d9b91e..1f4c39eb64a 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -8,14 +8,14 @@ describe ProjectDestroyWorker do
describe "#perform" do
it "deletes the project" do
- subject.perform(project.id, project.owner, {})
+ subject.perform(project.id, project.owner.id, {})
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
end
it "deletes the project but skips repo deletion" do
- subject.perform(project.id, project.owner, { "skip_repo" => true })
+ subject.perform(project.id, project.owner.id, { "skip_repo" => true })
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_truthy